v4.3.20: 미커밋 자산 일괄 정리 + 유저 워크스페이스/세션토큰 gitignore

그동안 쌓여 있던 미커밋 파일들을 정리. 소스·자산만 올리고, 개인 데이터와
런타임 상태는 gitignore로 차단.

추가된 자산:
- web-ui/audiomass, web-ui/wavacity — 자체 호스팅 오디오 에디터
- web-ui/drums — 드럼 샘플
- wol-gate — WOL 프록시 컨테이너(Dockerfile + server.js)
- glm-1m-fix — GLM 1M 컨텍스트 설치 스크립트(linux/windows)
- RENODE_FIXES_SUMMARY*.md, CCTV정보_서울특별시.csv, .claude/plans

수정:
- edge_tts_synth.py: rate 인자 추가(말하기 속도 조절)
- .smallclaw/config.json: google(gemini) provider 등록 — 키는 vault 참조
- assets/SmallClaw*.png 삭제분 반영

gitignore (중요):
- .smallclaw/users/*/workspace/ 전체 차단. 기존 패턴이 memory/uploads와 일부
  확장자만 막아서 detective/(고소장·통화녹취·의사소견서·가족관계증명서),
  generated-media/, music/, pptx/ 등 앱 생성 개인 데이터 403MB가 노출돼 있었음
- .smallclaw/active-sessions.json 차단 — 살아있는 bearer 토큰과 admin 역할이
  평문으로 들어있어 커밋 시 게이트웨이 관리자 자격증명이 그대로 공개됨
- google-usage.json, config.json.bak-*, workspace/memory·weather-maps·
  task_result_*, voice/ 등 런타임 상태도 함께 차단

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-29 14:55:37 +09:00
co-authored by Claude Opus 5
parent 8359901cac
commit e85ebe6608
127 changed files with 122199 additions and 7 deletions
+147
View File
@@ -0,0 +1,147 @@
# Arduino 에뮬레이터 센서/액추에이터 추가 계획
## 목표
`web-ui/arduino-emulator.html`과 `src/gateway/server-v2.ts`에 다음 5가지 부품의 시뮬레이션을 순서대로 추가한다.
1. **MPU-6050** (I2C 6축 IMU)
2. **BME280** (I2C 온도/습도/기압)
3. **DS18B20** (1-Wire 온도)
4. **릴레이 모듈** (GPIO 스위칭)
5. **L298N 모터 드라이버** (GPIO DC 모터)
## 적용 대상 에뮬레이터
- **ESP32 QEMU** (`src/gateway/server-v2.ts` QEMU 경로)
- **ESP8266 native** (`src/gateway/server-v2.ts` g++ native 경로)
- **AVR8js (UNO/Nano)**는 시뮬레이션 회로도(`SCH`)에서 DHT 연결 감지 패턴을 확장하여 일부 지원.
## 1단계: 공통 인프라 확장
### 1.1 백엔드 프로토콜 추가 (`_parsePtyGpio`)
`\x02` 프리픽스 명령어를 추가하여 SSE로 브로드캐스트.
| 명령어 | 의미 | SSE 페이로드 |
|---|---|---|
| `IMU:<sda>:<scl>:<addr>` | MPU-6050 초기화 | `{imu:{sda,scl,addr}}` |
| `IMV:<addr>:<ax>:<ay>:<az>:<gx>:<gy>:<gz>:<temp>` | IMU 값 변경 | `{imu_values:{addr,ax,ay,az,gx,gy,gz,temp}}` |
| `BMU:<sda>:<scl>:<addr>` | BME280 초기화 | `{bme:{sda,scl,addr}}` |
| `BMV:<addr>:<t>:<h>:<p>` | BME 값 변경 | `{bme_values:{addr,t,h,p}}` |
| `OWI:<pin>` | OneWire/DS18B20 초기화 | `{onewire:{pin}}` |
| `OWV:<pin>:<t>` | DS18B20 온도 | `{ds18b20:{pin,t}}` |
| `RLY:<pin>:<state>` | 릴레이 상태 | `{relay:{pin,state}}` |
| `MOT:<enA>:<in1>:<in2>:<in3>:<in4>:<enB>` | L298N 핀 설정 | `{motor:{enA,in1,in2,in3,in4,enB}}` |
| `MST:<enA>:<in1>:<in2>:<in3>:<in4>:<enB>` | L298N 상태 갱신 | `{motor_state:{...}}` |
### 1.2 값 주입 엔드포인트 추가
`/api/arduino/qemu/:id/imu`, `/esp8266/:id/imu` 등 POST 엔드포인트를 추가하여 stdin/serial로 `\x02ISET:<addr>:...`, `\x02BSET:...`, `\x02OSET:...` 주입.
## 2단계: MPU-6050 (I2C)
### 2.1 백엔드
- `Wire.h` 모의 라이브러리를 강화: `beginTransmission(addr)`, `requestFrom(addr, qty)`가 감지되면 `\x02IMU:<sda>:<scl>:<addr>\n` 출력.
- `MPU6050.h` 모의 라이브러리 추가 (또는 `MPU6050` 클래스가 `Wire`로 레지스터 읽는 일반 라이브러리 대응).
- `Wire.read()`가 호출될 때 내부 버퍼에서 가속도/자이로/온도 레지스터 값 반환.
- `_emb::_imu_poll()`에서 `ISET:<addr>:ax:ay:az:gx:gy:gz:temp` 파싱 후 버퍼 갱신.
### 2.2 프론트엔드
- `#imu-section` 패널 추가: ax/ay/az, gx/gy/gz, temp 슬라이더.
- SSE `msg.imu` → 패널 표시 및 핀 정보 저장.
- SSE `msg.imu_values` → UI 갱신.
- 슬라이더 변경 시 POST `/api/arduino/{qemu,esp8266}/:id/imu`.
## 3단계: BME280 (I2C)
### 3.1 백엔드
- `Wire` 모의 라이브러리에서 주소 `0x76`/`0x77` 감지 시 `\x02BMU:<sda>:<scl>:<addr>\n` 출력.
- `BME280.h` 또는 `Adafruit_BME280.h` 모의: 보정 계수/측정 레지스터 읽기 시 온도/습도/기압 값 반환.
- `_emb::_bme_poll()`에서 `BSET:<addr>:t:h:p` 파싱.
### 3.2 프론트엔드
- `#bme-section` 패널 추가: 온도/습도/기압 슬라이더.
- SSE `msg.bme` / `msg.bme_values` 처리.
- POST `/api/arduino/{qemu,esp8266}/:id/bme`.
## 4단계: DS18B20 (1-Wire)
### 4.1 백엔드
- `OneWire.h` 모의 라이브러리 추가: `search()` / `reset()` 호출 시 `\x02OWI:<pin>\n` 출력.
- `DallasTemperature.h` 모의: `getTempCByIndex()` 호출 시 버퍼의 온도 반환.
- `_emb::_ow_poll()`에서 `OSET:<pin>:temp` 파싱.
### 4.2 프론트엔드
- `#ds18b20-section` 패널 추가: 온도 슬라이더.
- SSE `msg.onewire` / `msg.ds18b20` 처리.
- POST `/api/arduino/{qemu,esp8266}/:id/ds18b20`.
## 5단계: 릴레이 (GPIO)
### 5.1 백엔드
- 릴레이는 스케치에서 `pinMode(pin, OUTPUT)` + `digitalWrite(pin, HIGH/LOW)`만 사용.
- 게이트웨이가 `G<pin>:<val>` 출력을 감시할 필요 없이, 릴레이 모듈 모의 라이브러리를 제공해 `\x02RLY:<pin>:<state>\n` 명시적으로 출력.
- `Relay.h` 모의: `begin(pin)` → `RLY:<pin>:0`, `on()` → `RLY:<pin>:1`, `off()` → `RLY:<pin>:0`.
### 5.2 프론트엔드
- `#relay-section` 패널에 핀별 ON/OFF 토글 버튼 표시.
- SSE `msg.relay` → 토글 상태 갱신.
- 사용자가 토글 클릭 시 `\x03BTN` 프로토콜(기존) 또는 별도 POST로 스케치에 HIGH/LOW 반영.
- 단순히 표시용이면 토글만; 양방향이려면 `POST /input`에 `BTN:<pin>:<level>` 전송.
## 6단계: L298N 모터 드라이버
### 6.1 백엔드
- `L298N.h` 모의 라이브러리 추가: `begin()`, `setSpeedA(s)`, `forwardA()`, `backwardA()`, `stopA()` 등.
- 상태 변경 시 `\x02MST:<enA>:<in1>:<in2>:<in3>:<in4>:<enB>\n` 출력.
- 초기화 시 `\x02MOT:...\n` 출력.
### 6.2 프론트엔드
- `#motor-section` 패널: 채널 A/B 속도 슬라이더, 방향 버튼(정/역/정지), 상태 텍스트.
- SSE `msg.motor` / `msg.motor_state` 처리.
- POST `/api/arduino/{qemu,esp8266}/:id/motor`로 속도/방향 주입.
## 7단계: AVR8js 회로도 연동
- `detectDHTConnections()` 함수를 일반화하여 `detectI2CConnections()` / `detectOneWireConnections()` 추가.
- MPU-6050/BME280/DS18B20/릴레이/L298N 부품의 터미널 위치와 보드 핀 매핑을 통해 AVR8js의 `portD/B/C` 및 I2C TWI 인터페이스에 연동.
- 단, AVR8js의 I2C는 이미 LCD/OLED TWI 에뮬레이션이 있으므로 MPU-6050/BME280를 TWI 슬레이브로 등록하는 방식으로 확장.
## 8단계: 빌드 및 검증
1. `npm run build` (TypeScript).
2. `systemctl --user restart homeclaw`.
3. 테스트 스케치:
- MPU-6050: `#include <MPU6050.h>` → `Wire.begin()`, `mpu.initialize()` → 가속도/자이로 출력.
- BME280: `#include <Adafruit_BME280.h>` → `bme.begin()` → 온습기압 출력.
- DS18B20: `#include <OneWire.h>`, `<DallasTemperature.h>` → `sensors.getTempCByIndex(0)`.
- 릴레이: `#include <Relay.h>` → `relay.on()`.
- L298N: `#include <L298N.h>` → 모터 회전.
## 우선순위 및 단계별 커밋
각 부품별로 독립 커밋:
1. `feat(emu): MPU-6050 I2C simulation`
2. `feat(emu): BME280 I2C simulation`
3. `feat(emu): DS18B20 1-Wire simulation`
4. `feat(emu): Relay module simulation`
5. `feat(emu): L298N motor driver simulation`
## 위험 및 완화
- **위험**: I2C 모의가 `Wire` 라이브러리 표준과 다르면 다양한 서드파티 라이브러리와 호환되지 않을 수 있음.
- **완화**: Adafruit/SparkFun 라이브러리에서 가장 흔히 사용하는 `beginTransmission`, `requestFrom`, `read` 흐름만 지원하고, 복잡한 write-then-read 시퀀스는 단순화.
- **위험**: QEMU stdin 쓰기 시 부팅 중 충돌 (DHT에서 이미 확인).
- **완화**: 센서 초기화 이벤트 수신 후 지연 시간을 두고 값을 전송, 부팅 직후 write는 피함.
- **위험**: 한 번에 너무 많은 파일 수정으로 회귀 버그 발생.
- **완화**: 단계별로 구현/빌드/재시작/테스트 후 다음 단계 진행.
+23
View File
@@ -89,6 +89,29 @@ chatbot*.png
.smallclaw/users/*/workspace/*.bat
.smallclaw/users/*/workspace/*.txt
# --- PER-USER WORKSPACE: BLANKET IGNORE (2026-07-29) ---
# The per-user patterns above only covered memory/uploads/attachments and a handful of
# file extensions, which left every app-generated content directory exposed — detective/
# (case evidence: 고소장, 통화녹취 m4a, 정신과 소견서, 가족관계증명서), generated-media/,
# music/, pptx/, satellite-images/, writer/, code/ and more. That is private user data
# (403MB of it) that must never reach the remote, so ignore the whole subtree instead of
# chasing each new app's output folder. Files already tracked here (prompts/*.md and other
# shipped templates) are unaffected — gitignore does not untrack them, and their edits
# still show up in git status. Use `git add -f` to track a genuinely new template.
.smallclaw/users/*/workspace/
# --- GATEWAY RUNTIME STATE / SECRETS (2026-07-29) ---
# active-sessions.json maps live bearer tokens -> {username, role:"admin"}; committing it
# would publish working admin credentials for the gateway. The rest is per-machine runtime
# state or a stale pre-vault config backup — none of it belongs in the repo.
.smallclaw/active-sessions.json
.smallclaw/google-usage.json
.smallclaw/config.json.bak-*
.smallclaw/workspace/memory/
.smallclaw/workspace/weather-maps/
.smallclaw/workspace/task_result_*.txt
.smallclaw/voice/
# --- ROOT WORKSPACE RUNTIME FILES ---
# Keep: SOUL.md, SELF.md, IDENTITY.md, USER.md, MEMORY.md, AGENTS.md, TOOLS.md, BOOT.md, README.md
# These are default templates that ship with SmallClaw — new users need them.
+11 -6
View File
@@ -21,7 +21,7 @@
"providers": {
"ollama": {
"endpoint": "http://localhost:11434",
"model": "mistral-large-3:675b-cloud"
"model": "gemma4:31b-cloud"
},
"lm_studio": {
"endpoint": "http://host.docker.internal:1234",
@@ -38,6 +38,11 @@
"openai_codex": {
"model": "gpt-5.3-codex"
},
"google": {
"endpoint": "https://generativelanguage.googleapis.com/v1beta/openai",
"api_key": "vault:llm.google.api_key",
"model": "gemini-3.5-flash-lite"
},
"anthropic": {
"api_key": "",
"model": "claude-sonnet-4-6"
@@ -45,12 +50,12 @@
}
},
"models": {
"primary": "mistral-large-3:675b-cloud",
"primary": "gemma4:31b-cloud",
"fallback": "kimi-k2.6:cloud",
"roles": {
"manager": "mistral-large-3:675b-cloud",
"executor": "mistral-large-3:675b-cloud",
"verifier": "mistral-large-3:675b-cloud",
"manager": "gemma4:31b-cloud",
"executor": "gemma4:31b-cloud",
"verifier": "gemma4:31b-cloud",
"background_task": ""
},
"profiles": {
@@ -376,4 +381,4 @@
"baseUrl": "https://col.applecherry.net",
"publicUrl": "https://ai.applecherry.net"
}
}
}
+63
View File
@@ -0,0 +1,63 @@
admin
login
wp-admin
dashboard
api
api/v1
api/v2
upload
uploads
backup
backups
config
.env
.git
.htaccess
robots.txt
sitemap.xml
index.php
index.html
test
dev
staging
old
tmp
temp
log
logs
static
assets
images
img
css
js
fonts
files
data
db
sql
phpmyadmin
phpinfo.php
info.php
shell.php
cmd.php
wp-login.php
wp-config.php
xmlrpc.php
user
users
register
signup
logout
profile
account
settings
panel
manager
webmail
mail
ftp
ssh
console
server-status
server-info
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
# Renode STM32 Emulator Fixes Summary
This document summarizes the fixes implemented to resolve issues with the Renode STM32 emulator in the HomeClaw project.
## Issues Identified
Based on the debugging log, the following issues were identified:
1. **Infinite Loop in Sketch Code**: The `while (!Serial)` construct in Arduino sketches causes an infinite loop in Renode because there's no USB Serial stack implementation.
2. **Incomplete Peripheral Mocks**: The RCC, PWR, and DBGMCU peripheral mocks were missing implementations for several registers that the STM32 HAL library attempts to access.
3. **Missing GPIO Support**: GPIO peripheral support was missing, which is essential for many Arduino sketches.
4. **UART Connection Issues**: Problems with UART socket connections between Renode and the web interface.
5. **Vector Table Reading Problems**: Renode was incorrectly reading vector table values from memory.
## Fixes Implemented
### 1. Enhanced Peripheral Mocks
Updated the Python scripts for RCC, PWR, and DBGMCU peripherals to handle all registers that the STM32 HAL library accesses:
- **RCC (Reset and Clock Control)**: Added support for all register offsets including APB2RSTR, APB1RSTR, AHBRSTR, and CFGR2.
- **PWR (Power Control)**: Added support for CSR register in addition to CR.
- **DBGMCU (Debug MCU)**: Added proper register offset handling.
### 2. Added GPIO Peripheral Support
Created a new GPIO mock script (`stm32f1_gpio.py`) that implements basic GPIO functionality for ports A, B, and C.
### 3. Improved REPL File
Created an enhanced REPL file (`stm32f103_improved.repl`) with better peripheral definitions and GPIO support. Fixed syntax issues to ensure compatibility with Renode's parser.
### 4. Enhanced Sketch Code Patching
Updated the sketch code patching logic to remove various forms of blocking loops that prevent execution in Renode:
- `while (!Serial) {}`
- `while (!Serial);`
- `while (!Serial) delay(1);`
### 5. Better UART Connection Handling
Improved the UART socket connection logic with:
- Extended retry mechanism (60 attempts instead of 30)
- Better error logging
- Periodic keepalive pings
- Proper error handling for socket events
### 6. Enhanced Error Handling and Logging
Added more comprehensive error handling and logging throughout the Renode session management code.
## Files Modified
1. `/home/kim/.local/lib/renode/scripts/pydev/stm32f1_rcc.py` - Enhanced RCC peripheral mock
2. `/home/kim/.local/lib/renode/scripts/pydev/stm32f1_flash.py` - Updated FLASH peripheral mock
3. `/home/kim/.local/lib/renode/scripts/pydev/stm32f1_pwr.py` - Enhanced PWR peripheral mock
4. `/home/kim/.local/lib/renode/scripts/pydev/stm32f1_dbgmcu.py` - Enhanced DBGMCU peripheral mock
5. `/home/kim/.local/lib/renode/scripts/pydev/stm32f1_gpio.py` - New GPIO peripheral mock
6. `/home/kim/homeclaw/stm32f103_improved.repl` - Improved REPL file
7. `/home/kim/homeclaw/src/gateway/routes-arduino.ts` - Updated Renode session handling
## Testing
A comprehensive test script was created to verify the fixes:
- `/home/kim/homeclaw/comprehensive_renode_test.sh`
The test compiles a sample sketch and prepares it for Renode execution.
## Usage
To test the fixes:
1. Open the web interface
2. Create a new sketch or use the test sketch at `/home/kim/homeclaw/test_sketches/renode_test.ino`
3. Select an STM32 board (e.g., Blue Pill F103C8)
4. Click the 'Emulator' button to start the Renode simulation
5. You should see output from the sketch in the serial monitor
## Future Improvements
1. Implement additional peripheral mocks as needed
2. Add support for more STM32 chip variants
3. Improve the GPIO implementation with more realistic behavior
4. Add support for interrupt handling
5. Implement more comprehensive peripheral functionality
These fixes should resolve the immediate issues with the Renode STM32 emulator and provide a more stable and functional emulation environment.
+87
View File
@@ -0,0 +1,87 @@
# Renode STM32 Emulator Fixes - FINAL
This document summarizes the final fixes implemented to resolve issues with the Renode STM32 emulator in the HomeClaw project.
## Issues Identified and Resolved
Based on the debugging log and testing, the following issues were identified and resolved:
1. **Infinite Loop in Sketch Code**: The `while (!Serial)` construct in Arduino sketches was causing an infinite loop in Renode because there's no USB Serial stack implementation.
2. **Peripheral Implementation Conflicts**: Our custom Python peripheral implementations were conflicting with the built-in SVD (System View Description) generated peripherals.
3. **UART Connection Issues**: Problems with UART socket connections between Renode and the web interface.
4. **Platform Description Issues**: Incorrect or incomplete platform descriptions were causing the emulator to crash.
## Fixes Implemented
### 1. Enhanced Sketch Code Patching
Updated the sketch code patching logic to remove various forms of blocking loops that prevent execution in Renode:
- `while (!Serial) {}`
- `while (!Serial);`
- `while (!Serial) delay(1);`
### 2. Corrected Platform Usage
Updated the Renode session handling to use the built-in platform descriptions with SVD-generated peripherals:
- Using `/home/kim/.local/lib/renode/platforms/cpus/stm32f103.repl` for STM32F103
- Leveraging SVD files for automatic peripheral generation
- Properly handling RCC register tags
### 3. Improved UART Connection Handling
Enhanced the UART socket connection logic with:
- Better error handling
- Proper retry mechanisms
- Connection status verification
### 4. Enhanced Error Handling and Logging
Added more comprehensive error handling and logging throughout the Renode session management code.
## Key Changes Made
### Updated Files:
1. `/home/kim/homeclaw/src/gateway/routes-arduino.ts` - Updated Renode session handling
2. Peripheral mock scripts were updated but ultimately not used in favor of SVD-generated peripherals
### Updated Functionality:
1. **Sketch Code Patching**: Enhanced to remove blocking loops
2. **Platform Selection**: Now uses built-in SVD-generated platforms
3. **UART Handling**: Improved connection and data flow
4. **Error Reporting**: Better logging and error messages
## Verification Results
The fixes have been successfully verified:
- ✅ Renode starts and runs without crashing
- ✅ Firmware executes successfully
- ✅ SVD-generated peripherals are used correctly
- ✅ UART connections can be established
- ✅ HomeClaw service is running with updates
## Current Status
The Renode STM32 emulator is now working correctly with the HomeClaw platform. The key insight was to leverage the built-in SVD-generated peripherals rather than trying to implement custom Python peripheral mocks, which was causing conflicts.
## Usage
To use the Renode STM32 emulator:
1. Open the web interface
2. Create a new sketch or use an existing one
3. Select an STM32 board (e.g., Blue Pill F103C8)
4. Click the 'Emulator' button to start the Renode simulation
5. Observe output in the serial monitor
## Future Improvements
While the current implementation is working, future improvements could include:
1. Enhancing the serial output handling for better real-time display
2. Adding more comprehensive peripheral support through SVD files
3. Improving the user interface feedback during emulator startup
4. Adding support for more STM32 chip variants
These fixes resolve the immediate issues with the Renode STM32 emulator and provide a stable and functional emulation environment.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# GLM context 보정 패치 v2 (Linux) — 옵션 3: 모델별 런타임 env 주입
#
# 설치 파일:
# ~/.claude/statusline-glm-fix.js stdin 보정 wrapper (표시 fallback)
# ~/.claude/launch-claude.sh 모델별 context 를 ollama에서 읽어
# CLAUDE_CODE_MAX_CONTEXT_TOKENS 로 주입하고 exec
# settings.json:
# statusLine -> statusline-glm-fix.js
# env.CLAUDE_CODE_MAX_CONTEXT_TOKENS 제거 (launch-claude.sh 가 모델별로 주입)
#
# 사용: bash install-linux.sh
# 이후 claude 실행: bash ~/.claude/launch-claude.sh (ollama launch claude 대신)
# 되돌리기: settings.json statusLine.command 를 원래 dist/index.js 경로로.
set -euo pipefail
HC="$HOME/.claude"
WRAPPER="$HC/statusline-glm-fix.js"
LAUNCH="$HC/launch-claude.sh"
SETTINGS="$HC/settings.json"
PATCHER="$(mktemp -t glm-patch.XXXXXX.js)"
trap 'rm -f "$PATCHER"' EXIT
mkdir -p "$HC"
# --- 1) statusline wrapper ---
cat > "$WRAPPER" <<'WRAPPER_EOF'
// Thin statusline wrapper: Claude Code reports context_window_size=200000 for
// non-Anthropic models like glm-5.2:cloud, but GLM's real context window is 1M.
// Patch the stdin before forwarding to the real claude-dashboard, so it shows
// e.g. "0/1M" instead of "0/200K". Leaves the plugin cache untouched.
// (launch-claude.sh 가 모델별 env를 주입하면 이 wrapper는 대부분 no-op 이지만,
// env 없이 그냥 실행한 경우의 표시 fallback 역할.)
// To revert: set settings.json statusLine.command back to the dist/index.js path.
const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function resolveDashboard() {
const base = path.join(os.homedir(), '.claude/plugins/cache/claude-dashboard/claude-dashboard');
let dirs;
try { dirs = fs.readdirSync(base); } catch { dirs = []; }
const ver = dirs.filter((d) => /^\d+\.\d+\.\d+$/.test(d)).sort((a, b) => {
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
return 0;
}).pop();
return ver ? path.join(base, ver, 'dist/index.js') : path.join(base, '1.30.0/dist/index.js');
}
const DASHBOARD = resolveDashboard();
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (c) => { raw += c; });
process.stdin.on('end', () => {
let payload = raw;
try {
const json = JSON.parse(raw);
const cw = json?.context_window;
const size = cw?.context_window_size;
const modelId = String(json?.model?.id || '').toLowerCase();
if (modelId.includes('glm') && size === 200000) {
cw.context_window_size = 1000000;
const usage = cw.current_usage;
if (usage) {
const input = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
cw.used_percentage = Math.round((input / 1000000) * 100);
if (typeof cw.remaining_percentage === 'number') cw.remaining_percentage = 100 - cw.used_percentage;
}
payload = JSON.stringify(json);
}
} catch { /* pass through */ }
const child = spawn('node', [DASHBOARD], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(payload);
child.stdin.end();
});
WRAPPER_EOF
# --- 2) launch-claude.sh ---
cat > "$LAUNCH" <<'LAUNCH_EOF'
#!/bin/bash
# claude를 모델별 실제 context window에 맞춰 실행.
# config.json 의 claude 모델 -> ollama show 로 context length -> CLAUDE_CODE_MAX_CONTEXT_TOKENS 주입 -> exec.
# compact 트리거 = min(AUTO_COMPACT_WINDOW, 모델 context) 이라 MAX_CONTEXT_TOKENS 만 주면 됨.
set -euo pipefail
CFG="$HOME/.ollama/config.json"
[ ! -f "$CFG" ] && { echo "⚠️ $CFG 없음 — env 없이 실행" >&2; exec ollama launch claude; }
MODEL=$(node -e 'const fs=require("fs"),os=require("os");const p=os.homedir()+"/.ollama/config.json";const d=JSON.parse(fs.readFileSync(p,"utf8"));const m=(d.integrations&&d.integrations.claude&&d.integrations.claude.models)||[];process.stdout.write(m[0]||"");' 2>/dev/null || true)
if [ -z "$MODEL" ]; then echo "⚠️ claude 모델 못 읽음 — env 없이 실행" >&2; exec ollama launch claude; fi
CTX=$(ollama show "$MODEL" 2>/dev/null | awk '/context length/{print $3; exit}' || true)
if ! [[ "$CTX" =~ ^[0-9]+$ ]] || [ "$CTX" -eq 0 ]; then echo "⚠️ '$MODEL' context length 못 읽음 — env 없이 실행" >&2; exec ollama launch claude; fi
echo "▶ model=$MODEL context=$CTX → CLAUDE_CODE_MAX_CONTEXT_TOKENS=$CTX" >&2
export CLAUDE_CODE_MAX_CONTEXT_TOKENS="$CTX"
exec ollama launch claude
LAUNCH_EOF
chmod +x "$LAUNCH"
# --- 3) settings.json 패치: statusLine 교체 + 정적 env 제거 ---
cat > "$PATCHER" <<'PATCHER_EOF'
const fs = require('fs');
const settingsPath = process.argv[2];
const wrapperPath = process.argv[3];
const s = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const beforeSL = s.statusLine && s.statusLine.command;
const beforeEnv = s.env && s.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
s.statusLine = { type: 'command', command: 'node ' + wrapperPath };
// launch-claude.sh 가 모델별로 env를 주입하므로 정적 env는 제거 (충돌 방지).
if (s.env) {
delete s.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
if (Object.keys(s.env).length === 0) delete s.env;
}
fs.writeFileSync(settingsPath, JSON.stringify(s, null, 2) + '\n');
console.log('statusLine before: ' + beforeSL);
console.log('statusLine after : ' + s.statusLine.command);
console.log('env removed (was): ' + (beforeEnv || '(none)'));
console.log('env now : ' + JSON.stringify(s.env || {}));
PATCHER_EOF
node "$PATCHER" "$SETTINGS" "$WRAPPER"
echo ""
echo "✅ 설치 완료."
echo " 이후 claude 실행은 ollama launch claude 대신"
echo " bash ~/.claude/launch-claude.sh 로 하세요 (모델별 context 자동 주입)."
+114
View File
@@ -0,0 +1,114 @@
# GLM context 보정 패치 v2 (Windows) — 옵션 3: 모델별 런타임 env 주입
#
# 설치 파일:
# %USERPROFILE%\.claude\statusline-glm-fix.js stdin 보정 wrapper (표시 fallback)
# %USERPROFILE%\.claude\launch-claude.ps1 모델별 context 를 ollama에서 읽어
# CLAUDE_CODE_MAX_CONTEXT_TOKENS 로 주입하고 exec
# settings.json:
# statusLine -> statusline-glm-fix.js
# env.CLAUDE_CODE_MAX_CONTEXT_TOKENS 제거 (launch-claude.ps1 가 모델별로 주입)
#
# 사용: powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
# 이후 claude 실행: powershell -ExecutionPolicy Bypass -File ~/.claude/launch-claude.ps1
# (ollama launch claude 대신. 단 Windows 에서 ollama launch 통합이 있을 때.)
# 되돌리기: settings.json statusLine.command 를 원래 dist/index.js 경로로.
$ErrorActionPreference = 'Stop'
$HC = Join-Path $env:USERPROFILE '.claude'
$Wrapper = Join-Path $HC 'statusline-glm-fix.js'
$Launch = Join-Path $HC 'launch-claude.ps1'
$Settings = Join-Path $HC 'settings.json'
$Patcher = Join-Path $env:TEMP "glm-patch-$([System.Guid]::NewGuid()).js"
New-Item -ItemType Directory -Force -Path $HC | Out-Null
# --- 1) statusline wrapper ---
$wrapperJs = @'
const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function resolveDashboard() {
const base = path.join(os.homedir(), '.claude/plugins/cache/claude-dashboard/claude-dashboard');
let dirs;
try { dirs = fs.readdirSync(base); } catch { dirs = []; }
const ver = dirs.filter((d) => /^\d+\.\d+\.\d+$/.test(d)).sort((a, b) => {
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
return 0;
}).pop();
return ver ? path.join(base, ver, 'dist/index.js') : path.join(base, '1.30.0/dist/index.js');
}
const DASHBOARD = resolveDashboard();
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (c) => { raw += c; });
process.stdin.on('end', () => {
let payload = raw;
try {
const json = JSON.parse(raw);
const cw = json?.context_window;
const size = cw?.context_window_size;
const modelId = String(json?.model?.id || '').toLowerCase();
if (modelId.includes('glm') && size === 200000) {
cw.context_window_size = 1000000;
const usage = cw.current_usage;
if (usage) {
const input = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
cw.used_percentage = Math.round((input / 1000000) * 100);
if (typeof cw.remaining_percentage === 'number') cw.remaining_percentage = 100 - cw.used_percentage;
}
payload = JSON.stringify(json);
}
} catch { /* pass through */ }
const child = spawn('node', [DASHBOARD], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(payload);
child.stdin.end();
});
'@
Set-Content -Path $Wrapper -Value $wrapperJs -Encoding UTF8
# --- 2) launch-claude.ps1 ---
$launchPs = @'
$ErrorActionPreference = 'Stop'
$Cfg = Join-Path $env:USERPROFILE '.ollama\config.json'
if (-not (Test-Path $Cfg)) { Write-Host "⚠️ $Cfg 없음 — env 없이 실행" -ForegroundColor Yellow; & ollama launch claude; return }
$Model = node -e "const fs=require('fs'),os=require('os');const p=os.homedir()+'/.ollama/config.json';const d=JSON.parse(fs.readFileSync(p,'utf8'));const m=(d.integrations&&d.integrations.claude&&d.integrations.claude.models)||[];process.stdout.write(m[0]||'');"
if (-not $Model) { Write-Host "⚠️ claude 모델 못 읽음 — env 없이 실행" -ForegroundColor Yellow; & ollama launch claude; return }
$line = ollama show $Model 2>$null | Select-String -Pattern 'context length'
$Ctx = ($line -split '\s+')[-1]
if (-not ($Ctx -match '^\d+$') -or $Ctx -eq '0') { Write-Host "⚠️ '$Model' context length 못 읽음 — env 없이 실행" -ForegroundColor Yellow; & ollama launch claude; return }
Write-Host "▶ model=$Model context=$Ctx → CLAUDE_CODE_MAX_CONTEXT_TOKENS=$Ctx" -ForegroundColor Cyan
$env:CLAUDE_CODE_MAX_CONTEXT_TOKENS = $Ctx
& ollama launch claude
'@
Set-Content -Path $Launch -Value $launchPs -Encoding UTF8
# --- 3) settings.json 패치: statusLine 교체 + 정적 env 제거 ---
$patcherJs = @'
const fs = require('fs');
const settingsPath = process.argv[2];
const wrapperPath = process.argv[3];
const s = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const beforeSL = s.statusLine && s.statusLine.command;
const beforeEnv = s.env && s.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
s.statusLine = { type: 'command', command: 'node ' + wrapperPath };
if (s.env) {
delete s.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
if (Object.keys(s.env).length === 0) delete s.env;
}
fs.writeFileSync(settingsPath, JSON.stringify(s, null, 2) + '\n');
console.log('statusLine before: ' + beforeSL);
console.log('statusLine after : ' + s.statusLine.command);
console.log('env removed (was): ' + (beforeEnv || '(none)'));
console.log('env now : ' + JSON.stringify(s.env || {}));
'@
Set-Content -Path $Patcher -Value $patcherJs -Encoding UTF8
& node $Patcher $Settings $Wrapper
Remove-Item -Path $Patcher -Force -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "✅ 설치 완료."
Write-Host " 이후 claude 실행은 ollama launch claude 대신"
Write-Host " powershell -ExecutionPolicy Bypass -File ~/.claude/launch-claude.ps1"
+2 -1
View File
@@ -9,7 +9,8 @@ async def main():
sys.exit(1)
voice = sys.argv[1] if len(sys.argv) > 1 else 'ko-KR-SunHiNeural'
output_file = sys.argv[2] if len(sys.argv) > 2 else '/tmp/tts_edge_out.mp3'
communicate = edge_tts.Communicate(text, voice)
rate = sys.argv[3] if len(sys.argv) > 3 else '+0%'
communicate = edge_tts.Communicate(text, voice, rate=rate)
await communicate.save(output_file)
asyncio.run(main())
+418
View File
@@ -0,0 +1,418 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>AudioMass - About</title>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool"/>
<meta property="og:image" content="https://audiomass.co/icon.jpg"/>
<meta property="og:title" content="AudioMass">
<meta property="og:url" content="https://audiomass.co/">
<meta property="og:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="keywords" content="AudioMass, WebAudio, WaveForm, audio editing, free audio editing, audio tool, waveform editor, sound editor, open source">
<link href="/ico.ico" rel="shortcut icon">
<style>
body{
padding:0;margin:0;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color:rgb(5, 10, 5);
background:#fefefe
}
#w{
width: 90%;
max-width:960px;
margin:0 auto
}
#w > div{
padding:20px 0 0 0
}
p{
line-height:21px;
font-size:16px;
margin-bottom:1.2em
}
h1{
font-size: 26px;
margin-top:35px
}
h5{
font-size:1em;
margin-bottom:0
}
.top{
max-width:100%;
outline:0;
display:inline-block
}
.mid{
max-width:504px;
width: 90%;
outline:0;
margin-bottom:16px;
display:inline-block
}
.nomarg{
margin-bottom:0
}
pre {
padding:12px 16px;
display:inline-block;
border-radius:6px;
background:#e1f3e9;
text-shadow:0 1px #fff;
border-bottom:1px solid #aaa;
max-width:90%;
overflow:scroll
}
ul {
padding:0;
list-style:none
}
li {
padding:0 0 3px 0;
color:#333;
font-size:0.92em
}
video {
max-width:660px;
width:90%;
}
::-moz-selection { background: #080808; color:#fff; text-shadow:none }
::selection { background: #080808; color:#fff; text-shadow:none }
</style>
</head>
<body>
<div id="w">
<div>
<h1>Introducing AudioMass (<a href="https://audiomass.co" target="_blank">https://audiomass.co</a>) an open-source web based audio and waveform editing tool.</h1>
<img class="top" src="/about/audiomass_top.jpg" />
<p>AudioMass lets you record, or use your existing audio tracks, and modify them by trimming, cutting, pasting or applying effects, from compression and paragraphic equalizers to reverb, delay, repair tools, pitch/speed profiles and multitrack mixdown. AudioMass also supports hotkeys, offline use and a responsive interface so quick edits stay quick.
it is written solely in plain old-school javascript, weighs under 100kb compressed and has no backend or framework dependencies.</p>
<img class="mid" src="/about/audiomass_support.jpg" />
<p>It also has very good browser and device support.</p>
<h5>:: Feature List ::</h5>
<ul>
<li><a href="#getting-started">Loading Audio, navigating the waveform, zoom and pan</a></li>
<li><a href="#waveform-tools">Visualization of frequency levels</a></li>
<li><a href="#waveform-tools">Peak and distortion signaling</a></li>
<li><a href="#waveform-tools">Cutting/Pasting/Trimming parts of the audio</a></li>
<li><a href="#waveform-tools">Inverting and Reversing Audio</a></li>
<li><a href="#waveform-tools">Zero-crossing selection tools</a></li>
<li><a href="#markers">Markers / trackers for cue points and quick jumps</a></li>
<li><a href="#beat-tools">Automatic beat detection, beat bars and snap-to-beat</a></li>
<li><a href="#recording-audio">Recording Audio</a></li>
<li><a href="#export-mp3">Exporting to mp3</a></li>
<li><a href="#effects">Modifying volume levels</a></li>
<li><a href="#effects">Fade In/Out</a></li>
<li><a href="#effects">Compressor</a></li>
<li><a href="#effects">Normalization</a></li>
<li><a href="#effects">Reverb</a></li>
<li><a href="#effects">Delay</a></li>
<li><a href="#effects">Distortion</a></li>
<li><a href="#effects">Pitch Shift</a></li>
<li><a href="#effects">Graph-based pitch / speed profiles</a></li>
<li><a href="#effects">Click, hum and edit repair</a></li>
<li><a href="#seamless-loops">Creating seamless loops with crossfade preview</a></li>
<li><a href="#undo-offline">Keeps track of states so you can undo mistakes</a></li>
<li><a href="#undo-offline">Offline support!</a></li>
<li><a href="#multitrack">Multitrack editing and mixdown</a></li>
<li><a href="#changelog">Recent changelog</a></li>
</ul>
<p>And all this, still under 100kb of JS!</p>
</div>
<div>
<h3 id="getting-started">Getting Started</h3>
<p>To get started, drag and drop an audio file, or try the included sample.<br />
Once the file is loaded and you can view the waveform, zoom in, pan around, or select a region.</p>
<img class="mid" src="/about/audiomass_2.jpg" />
<h3 id="waveform-tools">Waveform Tools</h3>
<p>The main editor is built around fast region editing. Select part of the waveform to cut, copy, paste, trim, silence, reverse, invert, or process only that section. The display also includes frequency visualization, peak/distortion signaling and zero-crossing selection, so you can spot obvious problems and avoid rough edit points while editing.</p>
<h3 id="markers">Markers / Trackers</h3>
<p>Markers are little named trackers you can drop on the waveform when a spot matters. Press <i>[M]</i>, double-click the top ruler, or right-click and choose <i>Add Marker Here</i>. You can drag them around, rename or delete them, jump between them with <i>[</i> and <i>]</i>, or hold <i>[Shift]</i> while jumping to quickly turn the space between markers into a selection. Simple, tiny, and very useful when you are editing something longer than a ringtone.</p>
<h3 id="beat-tools">Beat Tools</h3>
<p>AudioMass can detect a rough tempo, draw metronome beat bars and snap edits to those beats. It is a small helper for loops, repeats and timing fixes, without turning the editor into a full DAW screen when you do not need one.</p>
<h3 id="effects">Effects And Processing</h3>
<p>AudioMass includes the usual quick processing tools: gain, fade in/out, compressor, normalization, reverb, delay, distortion and pitch shifting. There is also a graph-based pitch/speed profile for ramps and Doppler-style moves, plus repair tools for clicks, hum and hard edit points. Effects can be previewed before applying, and most of them work on either the current selection or the whole file.</p>
<h3 id="seamless-loops">Creating Seamless Loops</h3>
<p>Select a region and use <i>Edit &gt; Seamless Loop</i>, or right-click the waveform and choose <i>Seamless Loop</i>. AudioMass opens a small loop preview where you can adjust the crossfade, trim silence from the edges, snap to smoother zero crossings, click around the preview to test the loop, repeat it, then apply it back to the file or open it in a new editor tab.</p>
<h3 id="undo-offline">Undo And Offline</h3>
<p>AudioMass keeps an undo stack for edits, so you can experiment without committing every change permanently. It also runs locally in the browser with no backend, and can keep working offline after it has been loaded.</p>
<h3 id="recording-audio">Recording Audio</h3>
<p>To record audio, simply press the Recording button, or the <i>[R]</i> key.</p>
<img class="mid" src="/about/audiomass_3.jpg" />
<h3 id="export-mp3">Exporting to mp3</h3>
<p>In order to export back to mp3, click on 'File', then 'Export to mp3', and follow the modal's instructions.</p>
<img class="mid" src="/about/audiomass_4.jpg" />
</div>
<hr />
<div>
<h3>The story behind AudioMass. And a short rant on web interfaces.</h3>
<p>I wrote AudioMass back in June 2018 and it stayed dormant on my hard disk until I decided to share it with the world today (July 13th, 19, but you might be seeing this in 2020. Hi!!).
It started as a personal small tool for quick visualization of waveforms. Later I added the ability to cut/copy/paste as well as fade in and out. And soon after good 'ol feature creep and perfectionism took over! Soon after, it turned to a challenge to see how close to a full-featured waveform editor it can get, whilst maintaining acceptable performance and small filesize.</p>
<p>In general I am a big fan of the interfaces of DAWs <i>(Digital Audio Workstations)</i>, they are extremely, complex, intricate, versatile, and they manage to remain visually pleasant even through their infinite options and knobs. Many times I feel the web has taken a very wrong turn, as amazing interfaces such as...
<p class="nomarg">Sonar</p>
<img class="mid" src="/about/sonar.jpg" />
<br />
<p class="nomarg">Fruity loops</p>
<img class="mid" src="/about/fruity.png" />
<br />
<p>Existed for more than 10-15 years now, while we are struggling with animating some rectangles for 60fps... So for AudioMass I wanted to try and make a fast and performant interface. Drawing inspiration from the examples I mentioned earlier rather than the tradiional web development practices. This is my unconvincing but truthfull excuse as to why the code is ugly; it is focused on being fast and getting the job done, with little regard on structure.</p>
<p>For the record: AudioMass was started in 2018, multitrack was added but never deployed in 2022, and to this day the thing is still about 90% handcrafted using older web technologies and a slightly old-school coding style. I keep working on it mostly because it is fun, which, in 2026, I have been reliably informed is still a valid reason to write software :)</p>
<p>Going forward I would like to slowly clean up the multitrack logic and redo the rendering fully in canvas (or maybe WebGPU, we'll see), and, if I can pull it off, try to elevate AudioMass into something that stands a little closer to a proper professional audio workflow on the web. It is also not particularly well-optimized for mobile right now, but hopefully that is something that can be smoothed out with time.</p>
</div>
<hr />
<div>
<h3>Building the interface</h3>
<p>Let's say we have a <i>"PLAY"</i> button and when we press it the track begins to play. I suppose we would want the button's color and state to reflect that the track is now playing. So naively we would do something like;</p>
<pre><code>btn.onclick = function () {
this.classList.add ('active');
};</code></pre>
<p>But what happens if we have a hotkey that triggers the same action? Let's say we press <i>[SPACEBAR]</i> and the track begins to play. Do we modify that button's class in the spacebar's handler?</p>
<pre><code>document.onkeypress = function ( e ) {
if ( e.keyCode === 32) {
e.preventDefault ();
document.querySelector ('.playbtn').classList.add ('active');
}
};</code></pre>
<p>And what happens, if there are 2 buttons, or one gets dynamically removed? Do we do selectAll and iterate? Hmmm...<br />
And if the track is playing and we hit <i>[SPACEBAR]</i> or the play button again, we need to stop playing. What do we do then? You can see how this becomes messy very quickly as everything gets very tighlty coupled together in a big dependency ball.</p>
<p>Introducing the observer pattern. Actions are represented by events. So the above logic and be expressed as;</p>
<pre><code>btn.onclick = function () {
FireEvent ('RequestTogglePlay');
};
On ('WillPlay', function () {
btn.classList.add ('active');
});
On ('WillStop', function () {
btn.classList.remove ('active');
});
document.onkeypress = function ( e ) {
if ( e.keyCode === 32) {
e.preventDefault ();
FireEvent ('RequestTogglePlay');
}
};
On ('RequestTogglePlay', function () {
if (track.is_playing) {
FireEvent ('WillStop');
track.stop ();
}
else {
FireEvent ('WillPlay');
track.play ();
}
});
track.onPlayStart = function () {
FireEvent ('DidPlay');
};
track.onPlayStop = function () {
FireEvent ('DidStop');
};
</code></pre>
<p>Now this is completely decoupled and dependency free! The button will set its state according to the events it receives, and both the button and the spacebar key rely on the same mechanisms.
You may notice the vocabulary we are using. <i>"Request"</i>, <i>"Will"</i> and <i>"Did"</i>. These are arbitrarilly chosen to impose some extra structure.<br /> <i>"Request"</i> denotes intent to perform an action, it is not guaranteed that the action will execute as there might be conditions preventing it (eg unitialized or still loading objects). <i>"Will"</i> means that the conditions passed and we are attempting to perform the action. And <i>"Did"</i> means that the action just got performed.<br />
It might be a bit too verbose, but it worked very well for structuring AudioMass's interface.</p>
</div>
<hr />
<div>
<h3>Dockable UI</h3>
<p>One thing I love about DAW interfaces, is that every window can be pulled out of the main host. I fondly remember having 3 screens full of VST plugins. So can we do the same in the browser?</p>
<video src="/about/dock_ui.mp4" autoplay muted playsinline controls loop></video>
<p>Yes! And it is using some of the oldest tricks in the book. Essentially we create a pop-up window with <i>window.open</i> and just pass buffers to its <i>documentWindow</i> object. Surprisingly it is quite performant on all browsers except IE Edge. I believe they are serializing in ascii or base64 every packet or something. Also Chrome has an interesting bug where you can't pass more than 512 byte buffers.</p>
<p>So for the undocked window, we call <i>window.open</i>. But how do we make it work when it is docked? It would be quite cumbersome to write the same functionality twice, once as a standalone page, and once as an in-page script. Luckily we can avoid that completely, and re-use the same page by using iframes.</p>
<p>The only difference is that the undocked version uses <i>window.opener</i> to refference its parent, whereas the iframe uses <i>window.parent</i>.</p>
</div>
<hr />
<div>
<h3 id="multitrack">Multitrack <small>(back in 2026!)</small></h3>
<img class="top" src="/about/multitrack.png" />
<p>So remember when I said the next big feature was going to be multitrack support? Well, it is finally here. AudioMass now has a proper multitrack mode where you can layer multiple tracks, mix them together and bounce the whole thing back down to a single file. It is essentially a small DAW bolted on top of the regular waveform editor, sharing the same engine, the same hotkeys and the same effects.</p>
<p>Hit the <i>MultiTrack</i> button up in the top header and you get a fresh session with a couple of empty channels. Drag and drop audio files onto a track, or anywhere on the canvas really, and they will land where you dropped them. Each file becomes a <i>clip</i> that you can grab and slide around freely on its lane.</p>
<p>Clips can be trimmed by dragging their edges, faded in or out by pulling the little corner handles, split at the playhead with <i>[Ctrl/Cmd + X]</i>, copied, duplicated, renamed and deleted. When two clips on the same track overlap, AudioMass automatically draws a crossfade in the overlap region so your transitions stay smooth instead of clicking. The crossfade follows the clips around. Drag either side and the curve readjusts on its own.</p>
<p>Each track header has the usual suspects: volume, pan, mute, solo and record-arm. The little round knobs are continuous (drag up/down) and double-clicking resets them. There is also a separate <i>Mixer</i> view if you prefer the vertical-strips-with-big-meters look while you are mixing.</p>
<p>Recording works just like in the single-track editor, except it records onto whichever track is armed. You can punch in, monitor with the meters, and the moment you stop, the recorded blob immediately becomes a regular clip you can move and edit like an imported file. Multiple takes stack into the same track without losing previous ones.</p>
<p>Effects can be applied to the whole clip or to a region within it. Processing goes through an <i>OfflineAudioContext</i>, so it is non-destructive until you commit, and you can preview it live before applying. The undo stack covers every clip-level edit too, so you can experiment freely.</p>
<p>The whole session, tracks, clips, fades, knob values, the lot, can be saved out as a <i>.amss</i> file. It is just a wrapper around the raw audio plus some JSON describing the layout, compressed with LZMA. Drop it back in later and you pick up exactly where you left off. To export the final mix use <i>Mixdown</i>, and everything gets bounced through an offline render to a single wav or mp3, like any normal export.</p>
<p>Snapping to clip edges, the playhead and the region works while dragging, and the whole thing is reasonably mobile-friendly with touch gestures, pinch-to-zoom on the timeline and long-press context menus. It is not Pro Tools, but for arranging takes, building podcasts or stitching ideas together quickly in a browser tab, it gets the job done :)</p>
<h5>:: Multitrack Keyboard Shortcuts ::</h5>
<ul>
<li><i>[Space]</i>: play / stop &nbsp;&nbsp;&nbsp; <i>[Shift + Space]</i>: pause</li>
<li><i>[R]</i>: toggle recording on the armed track</li>
<li><i>[&larr;]</i> / <i>[&rarr;]</i>: seek (auto-accelerates the longer you hold)</li>
<li><i>[Shift + &larr;]</i> / <i>[Shift + &rarr;]</i>: jump to region edges / start / end</li>
<li><i>[&uarr;]</i> / <i>[&darr;]</i>: select previous / next channel</li>
<li><i>[Tab]</i>: center the view on the playhead</li>
<li><i>[Ctrl/Cmd + X]</i>: split the selected clip at the playhead</li>
<li><i>[X]</i>: toggle a crossfade between overlapping clips</li>
<li><i>[Ctrl/Cmd + C]</i> / <i>[Ctrl/Cmd + V]</i>: copy / paste a clip</li>
<li><i>[Backspace]</i> / <i>[Delete]</i>: delete the selected clip</li>
<li><i>[Ctrl/Cmd + Z]</i> / <i>[Ctrl/Cmd + Y]</i>: undo / redo (covers every clip-level edit)</li>
<li><i>[Ctrl/Cmd + A]</i>: select the whole arrangement as a region</li>
<li><i>[Ctrl/Cmd + S]</i>: open the save / export menu</li>
<li><i>[Esc]</i>: close any open context menu</li>
</ul>
<p>The <i>Shift + key</i> variants of the above (eg. <i>Shift + C</i> for copy) work too. They were the originals from before the <i>Ctrl/Cmd</i> bindings were added, kept around so old muscle memory doesn't break.</p>
</div>
<hr />
<div>
<h3 id="changelog">Recent Changelog</h3>
<ul>
<li><b>July 2022</b>: Multitrack mode with clips, fades, mixer controls, recording and mixdown.</li>
<li><b>January 2026</b>: <i>.amss</i> session files, so multitrack projects can be saved and opened again later.</li>
<li><b>February 2026</b>: Pitch / Speed Profile with graph editing, finer controls, Doppler-style presets and live preview seeking.</li>
<li><b>March 2026</b>: Automatic beat detection, metronome bars and snap-to-beat editing.</li>
<li><b>April 2026</b>: New repair tools for clicks, hum and hard edit points.</li>
<li><b>May 2026</b>: Seamless Loop with crossfade preview, silence trim, zero-crossing snap, repeat and open-in-new-editor.</li>
<li><b>May 2026</b>: Small UI polish passes, including precise time boxes, zero-cross selection mode and update-ready reload notices.</li>
</ul>
</div>
<hr />
<div>
<h3>Future work and performance considerations</h3>
<hr />
<p>There is also a lot of room for improvement in almost all aspects.</p>
<p>First of all we can further reduce the filesize by around 20kb by removing the library we are using for rendering the waveform. We use only a fraction of its functionality so there is no reason to include it all.</p>
<p>We can also optimize a lot the rendering of the waveform. I heavily modified the library used to compute and draw only the visible range. However it is still clearing and re-drawing all of the canvas at each frame. We can take advantage of 2d Context's translate calls, and shift the canvas around instead of redrawing all of the pixels.</p>
<p>We can also move some operations to a background thread, such as the filters processing so that the UI does not freeze when applying a long chain of effects.</p>
<br />
<p>However, the biggest issue I encountered, is the Web Audio API itself. Every operation results in iterating over multiple long arrays per frame. Eventually the garbage collector fires and crackling is introduced. Only way to go around this is to use a small fftSize, but then the frequency range we have to work with is very narrow. Perhaps a pure WASM implementation would outperform trying to modify audio signals with JS. Only one way to find out I guess :) </p>
<p>Additionally, <i>decodeAudioData</i> provides no progress callback, and no way of cancelling it. So if you attempt to load a huge audio file, you will waste resources until it gets processed. There is no way around this currently and it can get annoying if you push a big file by mistake.</p>
</div>
<br /><br /><br />
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

+2301
View File
File diff suppressed because it is too large Load Diff
+338
View File
@@ -0,0 +1,338 @@
(function ( w, d, PKAE ) {
'use strict';
function AMSSFormat ( app ) {
var q = this;
var enc = new TextEncoder ();
var dec = new TextDecoder ();
function nameOf ( name ) {
name = (name || 'audiomass-session').trim ();
return /\.amss$/i.test ( name ) ? name : name + '.amss';
}
function isFile ( file ) {
return !!(file && /\.amss$/i.test (file.name || ''));
}
function ctx () {
return app.engine.wavesurfer.backend.ac;
}
function pairKey ( a, b ) {
return a < b ? a + ':' + b : b + ':' + a;
}
function writer () {
var a = [];
var tmp = new ArrayBuffer (4);
var dv = new DataView ( tmp );
var tb = new Uint8Array ( tmp );
function u8 ( n ) { a.push ( n & 255 ); }
function u16 ( n ) { u8 ( n ); u8 ( n >> 8 ); }
function f32 ( n ) {
dv.setFloat32 (0, n || 0, true);
a.push (tb[0], tb[1], tb[2], tb[3]);
}
function str ( s ) {
var b = enc.encode ( s || '' );
var l = Math.min (255, b.length);
u8 ( l );
for (var i = 0; i < l; ++i) u8 ( b[i] );
}
return {a:a, u8:u8, u16:u16, f32:f32, str:str};
}
q.IsBuffer = function ( buf ) {
if (!buf || buf.byteLength < 4) return false;
var u = new Uint8Array ( buf, 0, 4 );
return u[0] > 0 && u[1] === 65 && u[2] === 77 && u[3] === 83;
};
q.ReadFile = function ( file, cb ) {
if (!isFile ( file )) return false;
var r = new FileReader ();
r.onload = function () { cb ( r.result, file.name ); };
r.onerror = function () { cb ( null, file.name ); };
r.readAsArrayBuffer ( file );
return true;
};
q.ExportMultitrack = function ( name, st ) {
var wtr = writer ();
var parts = [];
var audio = [];
var track_map = {};
var clip_map = {};
var xf = [];
var tracks = st.tracks || [];
var clips = st.clips || [];
var xfs = st.xfades || {};
function audioIndex ( buffer, name ) {
for (var i = 0; i < audio.length; ++i)
if (audio[i].buffer === buffer) return i;
audio.push ({buffer:buffer, name:name || 'Audio'});
return audio.length - 1;
}
for (var i = 0; i < tracks.length; ++i)
track_map[tracks[i].id] = i;
for (i = 0; i < clips.length; ++i) {
if (!clips[i].buffer || track_map[clips[i].track] === undefined)
return false;
clip_map[clips[i].id] = i;
audioIndex ( clips[i].buffer, clips[i].name );
}
for (var k in xfs) {
var ids = k.split ( ':' );
if (clip_map[ids[0]] !== undefined && clip_map[ids[1]] !== undefined)
xf.push ([clip_map[ids[0]], clip_map[ids[1]]]);
}
if (tracks.length > 65534 || clips.length > 65534 ||
audio.length > 65534 || xf.length > 65534)
return false;
wtr.u8 (1); wtr.u8 (65); wtr.u8 (77); wtr.u8 (83);
wtr.u8 (0);
wtr.u16 (tracks.length);
wtr.u16 (clips.length);
wtr.u16 (audio.length);
wtr.u16 (xf.length);
wtr.u16 (st.selected_track && track_map[st.selected_track] !== undefined ? track_map[st.selected_track] : 65535);
wtr.u16 (st.selected_clip && clip_map[st.selected_clip] !== undefined ? clip_map[st.selected_clip] : 65535);
wtr.f32 (st.cursor);
wtr.f32 (st.marker);
wtr.f32 (st.px_per_sec);
wtr.f32 (st.row_h);
wtr.f32 (st.master_vol);
for (i = 0; i < tracks.length; ++i) {
var t = tracks[i];
wtr.u8 ((t.mute ? 1 : 0) | (t.solo ? 2 : 0) | (t.rec ? 4 : 0));
wtr.f32 (t.vol === undefined ? 1 : t.vol);
wtr.f32 (t.pan || 0);
wtr.f32 (t.h || 1);
wtr.str (t.name);
}
for (i = 0; i < audio.length; ++i) {
var b = audio[i].buffer;
wtr.u8 (b.numberOfChannels);
wtr.u16 (b.sampleRate & 65535);
wtr.u16 (b.sampleRate / 65536);
wtr.u16 (b.length & 65535);
wtr.u16 (b.length / 65536);
wtr.str (audio[i].name);
}
for (i = 0; i < clips.length; ++i) {
var c = clips[i];
wtr.u16 (track_map[c.track]);
wtr.u16 (audioIndex (c.buffer, c.name));
wtr.f32 (c.start);
wtr.f32 (c.in || 0);
wtr.f32 (c.out);
wtr.f32 (c.fi || 0);
wtr.f32 (c.fo || 0);
wtr.str (c.name);
}
for (i = 0; i < xf.length; ++i) {
wtr.u16 (xf[i][0]);
wtr.u16 (xf[i][1]);
}
while (wtr.a.length & 3) wtr.u8 (0);
parts.push (new Uint8Array (wtr.a));
for (i = 0; i < audio.length; ++i) {
b = audio[i].buffer;
for (var ch = 0; ch < b.numberOfChannels; ++ch)
parts.push (b.getChannelData ( ch ));
}
var meta = new ArrayBuffer (8);
var mu = new Uint8Array ( meta );
var md = new DataView ( meta );
mu[0] = 66; mu[1] = 80; mu[2] = 77;
md.setFloat32 (4, st.beat_bpm > 0 ? st.beat_bpm : 120, true);
parts.push ( meta );
meta = new ArrayBuffer (8);
mu = new Uint8Array ( meta );
var sig = (st.beat_sig || '4/4').split ('/');
mu[0] = 83; mu[1] = 73; mu[2] = 71;
mu[4] = sig[0] / 1 || 4;
mu[5] = sig[1] / 1 || 4;
parts.push ( meta );
if (st.markers && st.markers.length) {
var mb = enc.encode (JSON.stringify (st.markers));
meta = new ArrayBuffer (8);
mu = new Uint8Array ( meta );
md = new DataView ( meta );
mu[0] = 77; mu[1] = 82; mu[2] = 75;
md.setUint32 (4, mb.length, true);
parts.push ( meta );
parts.push ( mb );
if (mb.length & 3) parts.push (new Uint8Array (4 - (mb.length & 3)));
}
var blob = new Blob (parts, {type:'application/x-audiomass-session'});
var url = (w.URL || w.webkitURL).createObjectURL ( blob );
var a = d.createElement ('a');
a.href = url;
a.download = nameOf ( name );
a.style.display = 'none';
d.body.appendChild ( a );
a.click ();
setTimeout (function () {
(w.URL || w.webkitURL).revokeObjectURL ( url );
a.parentNode && a.parentNode.removeChild ( a );
}, 0);
return true;
};
q.DecodeMultitrack = function ( buf ) {
if (!q.IsBuffer ( buf )) return null;
try {
var dv = new DataView ( buf );
var o = 4;
function u8 () { return dv.getUint8 ( o++ ); }
function u16 () { var v = dv.getUint16 ( o, true ); o += 2; return v; }
function f32 () { var v = dv.getFloat32 ( o, true ); o += 4; return v; }
function str () {
var l = u8 ();
var s = dec.decode (new Uint8Array (buf, o, l));
o += l;
return s;
}
function none ( v ) { return v === 65535 ? null : v; }
if (dv.getUint8 (0) !== 1) return null;
u8 ();
var nt = u16 ();
var nc = u16 ();
var na = u16 ();
var nx = u16 ();
var sel_t = none ( u16 () );
var sel_c = none ( u16 () );
var st = {
track_uid: nt + 1,
clip_uid: nc + 1,
cursor: f32 (),
marker: f32 (),
px_per_sec: f32 (),
row_h: f32 (),
master_vol: f32 (),
beat_bpm: 120,
beat_sig: '4/4',
markers: [],
xfades: {},
tracks: [],
clips: []
};
for (var i = 0; i < nt; ++i) {
var fl = u8 ();
st.tracks.push ({
id: 'mt' + (i + 1),
mute: !!(fl & 1),
solo: !!(fl & 2),
rec: !!(fl & 4),
vol: f32 (),
pan: f32 (),
h: f32 (),
name: str ()
});
}
var audio = [];
for (i = 0; i < na; ++i)
audio.push ({
ch: u8 (),
rate: u16 () + u16 () * 65536,
len: u16 () + u16 () * 65536,
name: str ()
});
var raw_clips = [];
for (i = 0; i < nc; ++i)
raw_clips.push ({
track: u16 (),
audio: u16 (),
start: f32 (),
inp: f32 (),
out: f32 (),
fi: f32 (),
fo: f32 (),
name: str ()
});
var raw_xf = [];
for (i = 0; i < nx; ++i)
raw_xf.push ([u16 (), u16 ()]);
var need = (o + 3) & ~3;
for (i = 0; i < audio.length; ++i) {
var ai = audio[i];
if (!ai.ch || ai.ch > 32 || !ai.rate || !ai.len) return null;
need += ai.ch * ai.len * 4;
}
if (need > buf.byteLength) return null;
o = (o + 3) & ~3;
for (i = 0; i < audio.length; ++i) {
ai = audio[i];
ai.buffer = ctx ().createBuffer (ai.ch, ai.len, ai.rate);
for (var ch = 0; ch < ai.ch; ++ch) {
ai.buffer.getChannelData (ch).set (new Float32Array (buf, o, ai.len));
o += ai.len * 4;
}
}
while (buf.byteLength >= o + 8) {
var mu = new Uint8Array (buf, o, 4);
if (mu[0] === 66 && mu[1] === 80 && mu[2] === 77 && mu[3] === 0)
st.beat_bpm = dv.getFloat32 (o + 4, true) || 120;
else if (mu[0] === 83 && mu[1] === 73 && mu[2] === 71 && mu[3] === 0)
st.beat_sig = (dv.getUint8 (o + 4) || 4) + '/' + (dv.getUint8 (o + 5) || 4);
else if (mu[0] === 77 && mu[1] === 82 && mu[2] === 75 && mu[3] === 0) {
var ml = dv.getUint32 (o + 4, true);
if (ml > 65536 || o + 8 + ml > buf.byteLength) break;
try {
var parsed_markers = JSON.parse (dec.decode (new Uint8Array (buf, o + 8, ml)));
st.markers = parsed_markers && parsed_markers.length ? parsed_markers : [];
}
catch (e) {
st.markers = [];
}
o += 8 + ml;
o = (o + 3) & ~3;
continue;
}
o += 8;
}
for (i = 0; i < raw_clips.length; ++i) {
var rc = raw_clips[i];
if (!st.tracks[rc.track] || !audio[rc.audio]) return null;
st.clips.push ({
id: 'mc' + (i + 1),
track: st.tracks[rc.track].id,
start: rc.start,
in: rc.inp,
out: rc.out,
fi: rc.fi || 0,
fo: rc.fo || 0,
name: rc.name || audio[rc.audio].name,
buffer: audio[rc.audio].buffer
});
}
for (i = 0; i < raw_xf.length; ++i)
st.xfades[pairKey ('mc' + (raw_xf[i][0] + 1), 'mc' + (raw_xf[i][1] + 1))] = 1;
st.selected_track = sel_t === null || !st.tracks[sel_t] ?
(st.tracks[0] && st.tracks[0].id) :
st.tracks[sel_t].id;
st.selected_clip = sel_c === null || !st.clips[sel_c] ? null : st.clips[sel_c].id;
return st;
} catch (e) { return null; }
};
}
PKAE._deps.amss = AMSSFormat;
})( window, document, PKAudioEditor );
+168
View File
@@ -0,0 +1,168 @@
(function ( w, d ) {
'use strict';
var _v = '0.9',
_id = -1;
function PKAE () {
var q = this; // keeping track of current context
q.el = null; // reference of main html element
q.id = ++_id; // auto incremental id
q._deps = {}; // dependencies
w.PKAudioList[q.id] = q;
var events = {};
q.fireEvent = function ( eventName, value, value2 ) {
if (q.multitrack &&
typeof eventName === 'string' &&
eventName.substr (0, 7) === 'Request' &&
q.multitrack.Propagate &&
q.multitrack.Propagate ( eventName, value, value2 ))
{
return (true);
}
var group = events[eventName];
if (!group) return (false);
var l = group.length;
while (l-- > 0) {
group[l] && group[l] ( value, value2 );
}
};
q.listenFor = function ( eventName, callback ) {
if (!events[eventName])
events[eventName] = [ callback ];
else
events[eventName].unshift ( callback );
};
q.stopListeningFor = function ( eventName, callback ) {
var group = events[eventName];
if (!group) return (false);
var l = group.length;
while (l-- > 0) {
if (group[l] && group[l] === callback) {
group[l] = null; break;
}
}
};
q.stopListeningForName = function ( eventName ) {
var group = events[eventName];
if (!group) return (false);
events[eventName] = null;
};
q.wheelInfo = function ( e ) {
var m = e.deltaMode === 1 ? 16 : (e.deltaMode === 2 ? w.innerHeight : 1);
var x = (e.deltaX || 0) * m;
var y = (e.deltaY || 0) * m;
if (e.shiftKey && Math.abs (x) < Math.abs (y)) {
x = y;
y = 0;
}
return {
x: x,
y: y,
ax: Math.abs (x),
ay: Math.abs (y),
pinch: !!e.ctrlKey
};
};
q.wheelZoomFactor = function ( delta ) {
return Math.max (0.2, Math.min (5, Math.pow (1.0025, -delta)));
};
q.fadeGain = function ( p ) {
p = p < 0 ? 0 : (p > 1 ? 1 : p);
return p * p;
};
var scripts = {};
q.loadScript = function ( src, ok, fail ) {
if (scripts[src] === true) {
ok && ok ();
return ;
}
if (scripts[src]) {
scripts[src].push ([ ok, fail ]);
return ;
}
scripts[src] = [[ ok, fail ]];
var script = d.createElement ('script');
script.onload = function () {
var list = scripts[src];
scripts[src] = true;
for (var i = 0; i < list.length; ++i)
list[i][0] && list[i][0] ();
};
script.onerror = function () {
var list = scripts[src];
scripts[src] = null;
for (var i = 0; i < list.length; ++i)
list[i][1] && list[i][1] ();
};
script.src = src;
d.head.appendChild (script);
};
q.init = function ( el_id ) {
var el = d.getElementById( el_id );
if (!el) {
console.log ('invalid element');
return ;
}
q.el = el;
// init libraries
q.mrk = q._deps.mrk ? new q._deps.mrk ( q ) : null;
q.ui = new q._deps.ui ( q ); q._deps.uifx ( q );
q.engine = new q._deps.engine ( q );
q.state = new q._deps.state ( 96, q );
q.rec = new q._deps.rec ( q );
q.fls = new q._deps.fls ( q );
q.amss = q._deps.amss ? new q._deps.amss ( q ) : null;
q.multitrack = q._deps.multitrack ? new q._deps.multitrack ( q ) : null;
if (q.multitrack && /[?&]multitrack=1\b/.test(w.location.search)) {
q.multitrack.Toggle (true);
}
if (w.location.href.split('local=')[1]) {
var sess = w.location.href.split('local=')[1];
q.fls.Init (function () {
q.fls.GetSession (sess, function ( e ) {
if(e && e.id === sess )
{
q.engine.LoadDB ( e );
}
});
});
}
return (q);
};
// check if we are mobile and hide tooltips on hover
q.isMobile = (/iphone|ipod|ipad|android/).test
(navigator.userAgent.toLowerCase ());
};
!w.PKAudioList && (w.PKAudioList = []);
// ideally we do not want a global singleto refferencing our audio tool
// but since this is a limited demo we can safely do it.
w.PKAudioEditor = new PKAE ();
PKAudioList.push (w.PKAudioEditor); // keeping track in the audiolist array of our instance
})( window, document );
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"net/http"
_ "net/url"
"fmt"
"os/exec"
"strings"
"time"
)
func main() {
changeHeaderThenServe := func(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
if strings.Contains(r.URL.Path, ".wasm") {
w.Header().Add("Content-Type", "application/wasm")
}
if strings.Contains(r.URL.Path, ".js") {
var epoch = time.Unix(0, 0).Format(time.RFC1123)
var noCacheHeaders = map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
}
fmt.Println("Req:", r.Host, r.URL.Path)
h.ServeHTTP(w, r)
}
}
go func() {
cmd := exec.Command("open", "-a", "Google Chrome", "http://localhost:5055/")
cmd.Output()
}()
fmt.Printf("\nListening on http://localhost:5055 \n\n")
http.Handle("/", changeHeaderThenServe(http.FileServer(http.Dir("."))))
panic(http.ListenAndServe(":5055", nil))
}
+7
View File
@@ -0,0 +1,7 @@
import http.server
import socketserver
PORT = 5055
Handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", PORT), Handler)
print("serving at port", PORT)
httpd.serve_forever()
+37
View File
@@ -0,0 +1,37 @@
CACHE MANIFEST
# v1 - July 26, 2018
# v2 - May 25, 2020
# v6 - multitrack sample cancel refresh
/index-cache.html
/manifest.json
/ico.png
/icon.png
/all.css
/all.build.js
/recorder-worklet.js
/tempo-estimator.js
/tempo-worker.js
/wav.js
/lame.js
/flac.js
/libflac.js
/libflac.wasm
/lz4-block-codec-wasm.js
/lz4-block-codec.wasm
/rnn_denoise.js
/rnn_denoise.wasm
/fonts/icomoon.ttf
/fonts/icomoon.woff
/eq.html
/sp.html
/mix.html
/test.mp3
NETWORK:
/index-cache.html
# Fallback content
FALLBACK:
. /index-cache.html
+207
View File
@@ -0,0 +1,207 @@
(function ( win, doc, PKAE ) {
'use strict';
var activeMenu = [],
namespace = win,
contextStorage = {},
_id = 0;
var closeEvent = [ 'mousedown', 'touchup' ];
/**
* Goes through every single context instance and terminates
* it.
**/
var closeContext = function ( e, force ) {
if (activeMenu.length === 0) return ;
var el = e && (e.target || e.srcElement);
var cls = el && el.className;
if (!el || (cls + '').indexOf('_action') === -1 || force)
{
var l = activeMenu.length;
while (l--) terminate (activeMenu[ l ]);
activeMenu = [];
}
},
/**
* Go through every children element of the context el,
* and remove all listeners and added attributes, then remove it
* from the dom also
**/
terminate = function( e ) {
if (!e || !e.currentMenu) return false;
var children = e.currentMenu.getElementsByTagName('*'),
len = children.length;
while( len-- )
children[ len ].parentNode.removeChild( children[ len ] );
e.currentMenu.removeEventListener( closeEvent[0], stopPropagation );
doc.body.removeChild( e.currentMenu );
e.currentMenu = null;
return false;
},
/** stop propagation func, so that we don't have to use anonymous funcs **/
stopPropagation = function(e){ e.stopPropagation(); },
openContext = function( e, x, y ) {
closeContext(null);
activeMenu.push( e );
//go through all the options and make the div
var div = doc.createElement('div'),
a,
marginOffset = 4,
opts = e.options,
leftOffset = x - marginOffset,
topOffset = y - marginOffset,
width = 0, height = 0;
div.className = "pk_contextMenu " + e.menuClass;
div.id = e.token;
for( var i = 0, len = opts.length; i < len; ++i ) {
if( opts[ i ].isHTML )
{
a = doc.createElement('div');
a.innerHTML = opts[ i ].isHTML;
div.appendChild( a );
}
else {
a = doc.createElement('a');
a.className = 'pk_ctx_action';
a.cnt = 1;
a.innerHTML = opts[ i ].name;
a.callback = opts[ i ].callback;
a.addEventListener( 'click', a.callback, false );
div.appendChild( a );
}
}
e.currentMenu = div;
div.addEventListener( closeEvent[0], stopPropagation, false );
doc.body.appendChild( div );
width = div.offsetWidth;
height = div.offsetHeight;
if( win.innerWidth < ( leftOffset + width ) && win.innerHeight < ( topOffset + height ) )
div.style.cssText = "top:" + ( topOffset - height ) + "px;left:" + ( leftOffset - width ) + "px;";
else if( win.innerWidth < ( leftOffset + width ) )
div.style.cssText = "top:" + ( topOffset ) + "px;left:" + ( leftOffset - width ) + "px;";
else if( win.innerHeight < ( topOffset + height ) )
div.style.cssText = "top:" + ( topOffset - height ) + "px;left:" + ( leftOffset ) + "px;";
else
div.style.cssText = "top:" + ( topOffset ) + "px;left:" + ( leftOffset ) + "px;";
if (e.onOpen) {
e.onOpen ( e, div );
}
return false;
},
openMenu = function( e )
{
if (e) {
e.preventDefault();
e.stopPropagation();
}
else {
e = {pageX:0, pageY:0};
}
// ----
var instance = getInstance ( this );
var pageX = e.pageX || e.clientX + doc.documentElement.scrollLeft;
var pageY = e.pageY || e.clientY + doc.documentElement.scrollTop;
if (!instance) return false;
instance.curr_target = e.target || e.srcElement;
openContext ( instance, pageX, pageY );
},
getInstance = function( elem ) {
return contextStorage[ elem.getAttribute( 'data-token' ) ];
};
/**
* Context Menu Constructor
**/
var contextMenu = namespace.contextMenu = function( elem, options ) {
if (!(this instanceof contextMenu)) return new contextMenu( elem, options );
if (!options) options = {};
var open_events = ['contextmenu', 'longpress'];
this.elem = elem;
this.options = [];
this.menuClass = options.className || 'pk_open';
this.curr_target = null;
// modified context menu to open only when double click + no movement
// if (elem) elem.addEventListener( 'contextmenu', openMenu, false );
if (elem) elem.addEventListener( 'pk_ctxmn', openMenu, false );
this.token = ++_id;
if (elem) elem.setAttribute( 'data-token', this.token );
contextStorage[ this.token ] = this;
};
/**
* Wrapper to the private openMenu function
**/
contextMenu.prototype.open = function( e ) {
openMenu.call( this.elem, e );
};
contextMenu.prototype.close = function( e ) {
closeContext();
};
contextMenu.prototype.openWithToken = function( token, x, y ) {
openContext( contextStorage[ token ], x||0, y||0 );
};
/**
* Closes context and removes it fully
**/
contextMenu.prototype.destroy = function() {
// this.elem.removeEventListener( 'contextmenu', openMenu );
this.elem.removeEventListener( 'pk_ctxmn', openMenu );
closeContext();
contextStorage[ this.token ] = null;
return false;
},
/**
* Adds option
* @param string name of the option
* @param function to run when its chosen
* @param if this is set, then append the HTML instead of its name in its position
* @param initialization code to run when the object is appended to the dom
**/
contextMenu.prototype.addOption = function( name, callback, isHTML ) {
var q = this;
this.options.push({ "name" : name, "callback" : function( e ) {
callback && callback( q, q._open );
closeContext( q, true );
},
"isHTML" : isHTML
});
};
// todo touch controls too? ####
doc.addEventListener( closeEvent[0], closeContext, true );
doc.addEventListener( 'killCTX', closeContext, false );
PKAE._deps.ContextMenu = contextMenu;
})( window, document, PKAudioEditor );
+178
View File
@@ -0,0 +1,178 @@
(function( parent ) {
'use strict';
/** parent object, set in the end of the selfcalling function **/
var parent = parent || window,
/** instance of File Reader **/
reader,
readFile,
/**
* Removes class from element
* @param htmlObject target element
* @param string "class to be removed"
**/
removeClass = function( el, value ) {
if ( !el.className ) return false;
var classes = el.className.split(' '),
ret = [];
for( var i = 0, l = classes.length; i < l; ++i )
if( classes[i] != value )
ret.push( classes[ i ] );
el.className = ret.join(' ');
};
if( !window.FileReader || !document.addEventListener ) {
throw( "File API not supported" );
readFile = function(){ throw( "File API not supported" ); };
}
else {
reader = new FileReader();
readFile = function ( file, callback, method ) {
/** Error handler (throws error at the console) **/
reader.onerror = function( e ) {
var message,
lut = [ "File not found.", "File coulnot be opened",
"File couldnot be uploaded", "Couldnot read File", "File too large" ];
// http://www.w3.org/TR/FileAPI/#ErrorDescriptions
throw( lut[ ( e.target.error.code - 1 ) ] );
},
/** Success, calling the callback **/
reader.onloadend = function( e ) {
callback && callback( e.target.result, file.name );
reader.onloadend = null;
};
// the method is specified in the beginning of the file
reader[ method ]( file );
return false;
};
}
/**
* Drag n Drop Files module
* @param HTMLElement, could be the Body
* @param DOMElement/String, if a string is specified then a div will be built and appended to the body
* with that String as its id. If a dom element is passed, that will be used instead. This object
* acts as an overlay and the file should be droped to this object. If this object is null, then the first argument
* will be used as the overlay.
* @param Function, will be called with the file data, and the filename as its arguments
* @param String, possible values "text, binarytext, arrabuffer" decides how the file will be read
* if let null, defaults to text
* @param String, class name to be added to the overlay (default is '__fadingIn')
**/
parent.dragNDrop = function( body, overlay_id, callback, method, _clss ) {
var win = window;
// check to see if we are using a mobile device - no need for dragNdrop in devices
// that do not support it somehow yet
if( ( 'ontouchstart' in window ) )
return "mobile";
/** JS Object, used to define the file-reading method **/
var method_lut = {
'text' : 'readAsText',
'binary' : 'readAsBinaryText',
'arrayBuffer': 'readAsArrayBuffer'
},
/** class added/removed from overlay object **/
clss = _clss ? _clss : "__fadingIn",
method = method ? method_lut[ method ] : 'readAsText',
/** how many events cast (dragenter/dragleave) **/
entered = 0,
/**
* DOMElement sink for the drag events
* if left unspecified then the body inherits the role
**/
overlay = !!overlay_id ? overlay_id : body,
/**
* (void) if the overlay_id specified is a string, then a div with that id is built
* and appended to the body. Else the default is used
**/
_overlayBuilder = function() {
if( typeof overlay_id === "string" )
{
var tmp = document.createElement( 'div' );
overlay = document.createElement( 'div' );
overlay.id = overlay_id;
tmp.innerHTML = "Drag n drop Files!";
overlay.appendChild( tmp );
body.appendChild( overlay );
tmp = null;
}
},
/**
* JS Object
* The events Object contains various functions that control
* the behavior of the events fired
**/
events = {
/**
* (void) Prevents default action and bubbling up
**/
silencer : function( e ) {
e.preventDefault();
e.stopPropagation()
},
/**
* Shows message to drop file
**/
onDragEnter : function( e ) {
// overlay.className += " " + clss;
++entered;
setTimeout(function() {
if( entered > 1 )
entered = 1;
}, 10 )
},
/**
* Hides the overlay... twist included!
**/
onDragLeave : function( e ) {
--entered;
if( entered <= 0 )
{
removeClass( overlay, clss );
entered = 0;
}
},
/**
* Files dropped
**/
onDrop : function( e ) {
// prevent the event from bubbling/firing default
events.silencer( e );
// Hide the overlay
removeClass( overlay, clss );
entered = 0;
/** dropped files. **/
var files = e.dataTransfer.files,
len;
// If anything is wrong with the dropped files, exit.
if( !files || !files.length )
return false;
len = files.length;
while( len-- )
// iterate files array and load them
readFile( files[ len ], callback, method );
}
};
(function init() {
//_overlayBuilder();
// events initialization
body.parentNode.addEventListener( "dragenter", events.onDragEnter, false );
body.addEventListener( "dragleave", events.onDragLeave, false );
body.addEventListener( "dragover", events.silencer, false);
body.addEventListener( "drop", events.onDrop, false);
return false;
})( body );
};
})( window );
+3830
View File
File diff suppressed because it is too large Load Diff
+226
View File
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>window.update = function(){};</script>
<title>AudioMass - Frequency Analysis</title>
<style>
html,body{height:100%}
body{padding:0;margin:0;background:#111;position:relative;z-index:2;overflow:hidden;}
div{position:absolute;top:50%;left:0;right:0;z-index:1;margin-top:-18px;text-align:center;user-select:none;
font-size:28px;color:#191919;pointer-events:none;font-family:Arial}
#e, #d, #f{
cursor:pointer;
position: absolute;
top: 7px;
right: 7px;
display: block;
height: 20px;
z-index: 99999;
background: #eaeaea;
left:auto;
border-radius:4px;
color:#111;
font:9px/20px Arial,sans-serif;
width: auto;
padding: 0 6px;
text-align: center;
opacity:0.9;
user-select:none;
}
.b #e, .b #d{
top:12px;
}
#d{
min-width:40px;
}
.b #e, .b #f{
display:block;
}
#e{
right: 68px;
width: 10px;
line-height: 20px;
padding: 0px 6px;
text-align: center;
opacity: 0.9;
display:none;
}
#f{
left: 0;
right: 0;
top: 0;
padding: 0;
width: 100%;
line-height: 0;
border-radius: 0;
height: 4px;
background: #333;
display:none;
user-select:none;
-moz-user-select:none;
}
.c #f, #f:hover{
background:#3C3C3C;
}
</style>
<meta charset="utf-8" />
</head>
<body>
<canvas id="fr" width="600" height="188" style="width:100%;height:100%;display:block"></canvas>
<div>FREQUENCY ANALYSER</div>
<a id="d" onclick="dock()">DOCK</a>
<a id="e" onclick="remove()">X</a>
<a id="f" onmousedown="return drag(event)"></a>
<script>
var d = document;
var w = window;
var iframe = location.href.indexOf('?iframe') === -1 ? 0 : 1;
var canvas = d.getElementById('fr');
var ctx = canvas.getContext('2d', {alpha:true, antialias:false});
w.remove = function (){};
if (iframe) {
d.body.className = 'b';
d.getElementById ('d').textContent = 'UNDOCK';
w.remove = function () {
w.parent.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'eq', [1, 1]);
};
};
w.drag = function ( e ) {
e.preventDefault ();
e.stopPropagation ();
e.returnValue = false;
w.parent.PKAudioEditor.ui.Dock ('RequestDragI', 'eq', [e.screenX, e.screenY]);
return false;
};
w.dock = function () {
if (!iframe)
{
if (!w.opener || !w.opener.PKAudioEditor) {
return ;
}
w.opener.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'eq', [1, 1]);
w.close && w.close ();
}
else
{
var frm = w.parent.document.getElementById ('pk_fr' + 'eq');
var t = 1;
if (frm && frm.getBoundingClientRect) {
var rect = frm.getBoundingClientRect();
t = [(w.parent.screenLeft + rect.left + 100)||0, (w.parent.screenTop + rect.top + 25)||0];
}
w.parent.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'eq', [t, 0]);
}
};
setTimeout(function () {
if (!iframe)
{
if (!w.opener || !w.opener.PKAudioEditor) return ;
}
var WIDTH = w.innerWidth, HEIGHT = w.innerHeight;
if (canvas.width != WIDTH)
{
canvas.width = WIDTH;
canvas.height = HEIGHT;
}
var bufferLength = 240;
var value_changed = false;
ctx.clearRect (0, 0, WIDTH, HEIGHT);
function draw( similarity ) {
var WIDTH = window.innerWidth, HEIGHT = window.innerHeight;
if (canvas.width != WIDTH || canvas.height != HEIGHT)
{
canvas.width = WIDTH;
canvas.height = HEIGHT;
}
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
var barWidth = (WIDTH / bufferLength);
var barHeight;
var x = 0;
for(var i = 0; i < bufferLength; i += 1) {
barHeight = similarity[i * 2]; // ? similarity[i] : 0;
// bar height normalize with 255
var newheight = ((barHeight / 256) * HEIGHT) >> 0;
ctx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)';
ctx.fillRect (x,HEIGHT - newheight, barWidth, newheight);
x += barWidth;// + 1;
}
value_changed = false;
};
w.draw = draw;
w.onunload = function () {
w.destroy && w.destroy ( iframe );
w.destroy = null;
};
w.update = function (freq_arr) {
if (!freq_arr)
ctx.clearRect (0, 0, WIDTH, HEIGHT);
else {
if (value_changed) return ;
value_changed = true;
window.requestAnimationFrame(function () {
draw (freq_arr);
});
}
};
var last_press = 0;
document.addEventListener ('keypress', function ( e ) {
if (e.keyCode !== 32) return ;
e.preventDefault ();
e.stopPropagation ();
if (e.timeStamp - last_press < 100) {
return ;
}
last_press = e.timeStamp;
if (!iframe) {
w.opener && w.opener.PKAudioEditor.ui.Dock ('RequestKeyDown', 32);
}
else {
w.parent && w.parent.PKAudioEditor.ui.Dock ('RequestKeyDown', 32);
}
});
}, 60);
</script>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
// FLAC worker for encoding audio using libflac.js
importScripts('libflac.js');
var flacEncoder;
var FLAC_INITIALIZED = false;
var sample_rate = 44100;
var compression = 5; // Default compression (0-8)
var channels = 1;
var total_samples = 0;
var buffers = [];
var bufIndex = 0;
var first_buffer = true;
var samples_left = null;
var samples_right = null;
function convert(n) {
var v = n < 0 ? n * 32768 : n * 32767;
return Math.max(-32768, Math.min(32767, v));
}
function initFLAC() {
if (FLAC_INITIALIZED) return true;
// SAMPLERATE, CHANNELS, BPS, COMPRESSION, SAMPLES, VERIFY, BLOCK_SIZE);
flacEncoder = Flac.create_libflac_encoder(sample_rate, channels, 16, compression, total_samples, true, 0);
if (flacEncoder != 0) {
var status = Flac.init_encoder_stream(flacEncoder, function(buffer, bytes) {
buffers.push(new Uint8Array(buffer));
bufIndex += buffer.byteLength;
});
FLAC_INITIALIZED = true;
return status == 0;
}
return false;
}
function interleave(inputL, inputR) {
var length = inputL.length + inputR.length;
var result = new Int32Array(length);
var index = 0,
inputIndex = 0;
while (index < length) {
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
++inputIndex;
}
return result;
}
onmessage = function(ev) {
if (!ev.data) return;
if (ev.data.sample_rate) {
sample_rate = ev.data.sample_rate / 1;
compression = ev.data.flac_compression;
channels = ev.data.channels / 1;
total_samples = ev.data.samples / 1 || 0;
initFLAC();
return;
}
if (first_buffer) {
samples_left = new Int16Array(ev.data, 0);
first_buffer = false;
if (channels > 1) return;
}
if (ev.data && channels > 1) {
samples_right = new Int16Array(ev.data, 0);
}
if (!FLAC_INITIALIZED) {
postMessage({percentage: 0});
return;
}
// Progress update
postMessage({percentage: 50});
// Process the audio data
var interleaved = null;
var samples = null;
if (channels > 1) {
// Create interleaved buffer for stereo
interleaved = interleave(samples_left, samples_right);
samples = interleaved;
} else {
// Use mono buffer directly
samples = samples_left;
}
// Encode the audio data
if (channels > 1) {
Flac.FLAC__stream_encoder_process_interleaved(flacEncoder, samples, samples_left.length);
} else {
var tmp_samples = new Int32Array(samples_left.length);
var tmp_index = 0;
while (tmp_index < samples_left.length) {
tmp_samples[tmp_index] = samples_left[tmp_index];
++tmp_index;
}
Flac.FLAC__stream_encoder_process(flacEncoder, [tmp_samples], samples_left.length);
}
// Finish encoding
Flac.FLAC__stream_encoder_finish(flacEncoder);
// Combine all buffers into a single Uint8Array
var outputData = new Uint8Array(bufIndex);
var offset = 0;
for (var i = 0; i < buffers.length; i++) {
outputData.set(buffers[i], offset);
offset += buffers[i].length;
}
// Create blob and return
var blob = new Blob([outputData], {type: 'audio/flac'});
postMessage(blob);
// Clean up
Flac.FLAC__stream_encoder_delete(flacEncoder);
FLAC_INITIALIZED = false;
buffers = [];
bufIndex = 0;
}
+1
View File
@@ -0,0 +1 @@
importScripts("libflac.js");var flacEncoder,FLAC_INITIALIZED=!1,sample_rate=44100,compression=5,channels=1,total_samples=0,buffers=[],bufIndex=0,first_buffer=!0,samples_left=null,samples_right=null;function convert(e){return Math.max(-32768,Math.min(32768,e<0?32768*e:32767*e))}function initFLAC(){var e;return!!FLAC_INITIALIZED||0!=(flacEncoder=Flac.create_libflac_encoder(sample_rate,channels,16,compression,total_samples,!0,0))&&(e=Flac.init_encoder_stream(flacEncoder,function(e,a){buffers.push(new Uint8Array(e)),bufIndex+=e.byteLength}),FLAC_INITIALIZED=!0,0==e)}function interleave(e,a){for(var n=e.length+a.length,s=new Int32Array(n),r=0,t=0;r<n;)s[r++]=e[t],s[r++]=a[t],++t;return s}onmessage=function(e){if(e.data)if(e.data.sample_rate)sample_rate=+e.data.sample_rate,compression=e.data.flac_compression,channels=+e.data.channels,total_samples=+e.data.samples||0,initFLAC();else if(!(first_buffer&&(samples_left=new Int16Array(e.data,0),first_buffer=!1,1<channels)))if(e.data&&1<channels&&(samples_right=new Int16Array(e.data,0)),FLAC_INITIALIZED){postMessage({percentage:50});e=null,e=1<channels?interleave(samples_left,samples_right):samples_left;if(1<channels)Flac.FLAC__stream_encoder_process_interleaved(flacEncoder,e,samples_left.length);else{for(var a=new Int32Array(samples_left.length),n=0;n<samples_left.length;)a[n]=samples_left[n],++n;Flac.FLAC__stream_encoder_process(flacEncoder,[a],samples_left.length)}Flac.FLAC__stream_encoder_finish(flacEncoder);for(var s=new Uint8Array(bufIndex),r=0,t=0;t<buffers.length;t++)s.set(buffers[t],r),r+=buffers[t].length;e=new Blob([s],{type:"audio/flac"});postMessage(e),Flac.FLAC__stream_encoder_delete(flacEncoder),FLAC_INITIALIZED=!1,buffers=[],bufIndex=0}else postMessage({percentage:0})};
BIN
View File
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe925;" glyph-name="files-empty" d="M917.806 602.924c-22.21 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-368c-44.114 0-80-35.888-80-80v-736c0-44.112 35.886-80 80-80h608c44.112 0 80 35.888 80 80v496c0 14.332-4.372 39.35-42.194 90.924zM785.374 657.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.982-17.78 50.678-41.878 81.374-72.572v0zM896 16c0-8.672-7.328-16-16-16h-608c-8.672 0-16 7.328-16 16v736c0 8.672 7.328 16 16 16 0 0 367.956 0.002 368 0v-224c0-17.672 14.324-32 32-32h224v-496zM602.924 917.804c-51.574 37.822-76.592 42.196-90.924 42.196h-368c-44.112 0-80-35.888-80-80v-736c0-38.632 27.528-70.958 64-78.39v814.39c0 8.672 7.328 16 16 16h486.876c-9.646 7.92-19.028 15.26-27.952 21.804z" />
<glyph unicode="&#xe926;" glyph-name="file-text2" d="M917.806 730.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 785.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896 16c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624zM736 128h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 256h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 384h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32z" />
<glyph unicode="&#xe987;" glyph-name="zoom-in" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256zM448 768h-128v-128h-128v-128h128v-128h128v128h128v128h-128z" />
<glyph unicode="&#xe988;" glyph-name="zoom-out" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256zM192 640h384v-128h-384z" />
<glyph unicode="&#xe996;" glyph-name="hammer" d="M1009.996 131.024l-301.544 301.544c-18.668 18.668-49.214 18.668-67.882 0l-22.626-22.626-184 184 302.056 302.058h-320l-142.058-142.058-14.060 14.058h-67.882v-67.882l14.058-14.058-206.058-206.060 160-160 206.058 206.058 184-184-22.626-22.626c-18.668-18.668-18.668-49.214 0-67.882l301.544-301.544c18.668-18.668 49.214-18.668 67.882 0l113.136 113.136c18.67 18.666 18.67 49.214 0.002 67.882z" />
<glyph unicode="&#xea1c;" glyph-name="play3" d="M192 832l640-384-640-384z" />
<glyph unicode="&#xea1d;" glyph-name="pause2" d="M128 832h320v-768h-320zM576 832h320v-768h-320z" />
<glyph unicode="&#xea1e;" glyph-name="stop2" d="M128 832h768v-768h-768z" />
<glyph unicode="&#xea1f;" glyph-name="backward2" d="M576 800v-320l320 320v-704l-320 320v-320l-352 352z" />
<glyph unicode="&#xea20;" glyph-name="forward3" d="M512 96v320l-320-320v704l320-320v320l352-352z" />
<glyph unicode="&#xea21;" glyph-name="first" d="M128 64v768h128v-352l320 320v-320l320 320v-704l-320 320v-320l-320 320v-352z" />
<glyph unicode="&#xea22;" glyph-name="last" d="M896 832v-768h-128v352l-320-320v320l-320-320v704l320-320v320l320-320v352z" />
<glyph unicode="&#xea23;" glyph-name="previous2" d="M256 64v768h128v-352l320 320v-704l-320 320v-352z" />
<glyph unicode="&#xea24;" glyph-name="next2" d="M768 832v-768h-128v352l-320-320v704l320-320v352z" />
<glyph unicode="&#xea2d;" glyph-name="loop" d="M128 640h640v-192l256 256-256 256v-192h-768v-384h128zM896 256h-640v192l-256-256 256-256v192h768v384h-128z" />
<glyph unicode="&#xea5a;" glyph-name="scissors" d="M913.826 280.306c-66.684 104.204-181.078 150.064-255.51 102.434-6.428-4.116-12.334-8.804-17.744-13.982l-79.452 124.262 183.462 287.972c15.016 27.73 20.558 60.758 13.266 93.974-6.972 31.75-24.516 58.438-48.102 77.226l-12.278 7.808-217.468-340.114-217.47 340.114-12.276-7.806c-23.586-18.79-41.13-45.476-48.1-77.226-7.292-33.216-1.75-66.244 13.264-93.974l183.464-287.972-79.454-124.262c-5.41 5.178-11.316 9.868-17.744 13.982-74.432 47.63-188.826 1.77-255.51-102.434-66.68-104.2-60.398-227.286 14.032-274.914 74.43-47.632 188.824-1.77 255.508 102.432l164.286 257.87 164.288-257.872c66.684-104.202 181.078-150.064 255.508-102.432 74.428 47.63 80.71 170.716 14.030 274.914zM234.852 159.57c-30.018-46.904-68.534-69.726-94.572-75.446-0.004 0-0.004 0-0.004 0-8.49-1.868-20.294-3.010-28.324 2.128-8.898 5.694-14.804 20.748-15.8 40.276-1.616 31.644 9.642 68.836 30.888 102.034 30.014 46.906 68.53 69.726 94.562 75.444 8.496 1.866 20.308 3.010 28.336-2.126 8.898-5.694 14.802-20.75 15.798-40.272 1.618-31.65-9.64-68.84-30.884-102.038zM480 448c-17.672 0-32 14.328-32 32s14.328 32 32 32 32-14.328 32-32-14.328-32-32-32zM863.85 126.53c-0.996-19.528-6.902-34.582-15.8-40.276-8.030-5.138-19.834-3.996-28.324-2.128 0 0 0 0-0.004 0-26.040 5.718-64.554 28.542-94.572 75.446-21.244 33.198-32.502 70.388-30.884 102.038 0.996 19.522 6.9 34.578 15.798 40.272 8.028 5.136 19.84 3.992 28.336 2.126 26.034-5.716 64.548-28.538 94.562-75.444 21.246-33.198 32.502-70.39 30.888-102.034z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
+500
View File
@@ -0,0 +1,500 @@
(function ( w, d, PKAE ) {
'use strict';
var _pid = 0;
var _aid = 0;
function FXAutomation ( app, filter_modal, val_cb, preview_cb ) {
var q = this;
q.modal = filter_modal;
q.app = app;
q.wv = app.engine.wavesurfer;
var mt = app.multitrack;
var mt_buffer = mt && mt.IsOn && mt.IsOn () && mt.GetFxBuffer ?
mt.GetFxBuffer () :
null;
if (mt_buffer) q.wv = {
backend:{buffer:mt_buffer},
regions:{list:[]},
getDuration:function () { return mt_buffer.duration; }
};
q.points = {};
q.act = null;
q.act_point = null;
q.in_auto = false;
q.rbuff = null;
q.waveDarken = filter_modal.waveDarken || 0;
q.btn_auto = _make_btn_auto ( q );
q.GetValue = function () {
var data = [];
var inputs = q.modal.el_body.getElementsByTagName('input');
var plen = q.points.length;
for (var i = 0; i < inputs.length; ++i)
{
var curr = inputs[i];
if (q.points[curr.id])
{
var arr = [];
var p = q.points[curr.id];
for (var j = 0; j < p.length; ++j)
{
var tmp = {
time: p[j].time,
val: p[j].val
};
arr.push(tmp);
val_cb && val_cb (tmp, curr);
}
data.push (arr);
}
else
{
var tmp = {
val: curr.value
};
data.push (tmp);
val_cb && val_cb (tmp, curr);
}
}
return (data);
};
q.DelAct = function ( min ) {
var p = q.act && q.points[q.act.id], i = p && p.indexOf (q.act_point);
if (!p || i < 0 || p.length <= min) return ;
p.splice (i, 1);
q.act_point = p[Math.min (i, p.length - 1)];
q.Render ();
return 1;
};
q.cw = 500;
q.ch = 200;
var els = _make_canvas ( q, q.cw, q.ch );
q.canvas = els[0];
q.ctx = els[1];
var _fillstyle = '#d9d955';
q.Render = function () {
var ctx = q.ctx;
var cw = q.cw;
var ch = q.ch;
if (q.rbuff) {
q.app.engine.GetWave (q.rbuff, 500, 200, null, null, q.canvas, q.ctx);
if (q.waveDarken) {
ctx.fillStyle = 'rgba(0,0,0,' + q.waveDarken + ')';
ctx.fillRect (0, 0, cw, ch);
}
}
// ctx.clearRect (0, 0, q.cw, q.ch);
ctx.fillStyle = _fillstyle;
ctx.strokeStyle = '#FF0000';
if (!q.act) return ;
ctx.beginPath ();
ctx.moveTo ( 0, ch / 2 );
var last_y = ch / 2;
for (var o = 0; o < q.points[q.act.id].length; ++o)
{
var curr = q.points[q.act.id][ o ];
var center_x = curr.ax;
var center_y = curr.ay;
ctx.lineTo ( center_x, center_y );
last_y = center_y;
}
ctx.lineTo ( cw, last_y );
ctx.stroke ();
var radius = 6;
for (var o = 0; o < q.points[q.act.id].length; ++o)
{
var curr = q.points[q.act.id][ o ];
var center_x = curr.ax;
var center_y = curr.ay;
ctx.beginPath ();
ctx.arc (center_x, center_y, radius, 0, 2 * Math.PI, false);
if (curr === q.act_point) {
ctx.shadowBlur = 24;
if (curr._on)
ctx.fillStyle = '#fff';
else
ctx.fillStyle = '#686868';
ctx.stroke ();
ctx.fill ();
ctx.shadowBlur = 0;
ctx.fillStyle = _fillstyle;
}
else if (curr._hov) {
if (curr._on)
ctx.fillStyle = 'blue';
else
ctx.fillStyle = 'darkblue';
ctx.stroke ();
ctx.fill ();
ctx.fillStyle = _fillstyle;
}
else if (curr._on) {
ctx.fill ();
}
else {
ctx.fillStyle = '#555';
ctx.fill ();
ctx.fillStyle = _fillstyle;
}
}
};
_make_controls ( q );
// -------
function _make_controls ( q ) {
var click_time = 0, seek_t = 0, no_seek = 0;
q.canvas.addEventListener ('click', function ( e ) {
if (!q.act) return;
var h = q.app.engine.FXPreviewHost;
if (seek_t) { clearTimeout (seek_t); seek_t = 0; }
if (e.timeStamp - click_time < 260)
{
var bounds = q.canvas.getBoundingClientRect ();
var cw = q.cw;
var ch = q.ch;
var posx = e.clientX - bounds.left;
var posy = e.clientY - bounds.top;
var rel_x = posx / cw;
var rel_y = posy / ch;
if (!q.points[q.act.id]) q.points[q.act.id] = [];
var duration;
var region = q.wv.regions.list[0];
if (region) {
duration = region.end - region.start;
} else {
duration = q.wv.getDuration();
}
q.points[q.act.id].push ({
// el:q.act.el,
id: ++_pid,
x: rel_x,
y: rel_y,
ax: rel_x * cw,
ay: rel_y * ch,
time: duration * rel_x,
val : ((1 - rel_y) * (q.act.max - q.act.min)) + q.act.min,
_on: true,
_hov: false,
});
q.points[q.act.id].sort( _compare );
q.act_point = q.points[q.act.id][q.points[q.act.id].length - 1];
//_process ( q, q.wv.backend.buffer );
q.Render ();
// ----
}
else if (!no_seek && preview_cb && h && (h.previewing || h.MTPreviewing))
{
var bounds = q.canvas.getBoundingClientRect ();
var sx = Math.max (0, Math.min (1, (e.clientX - bounds.left) / bounds.width));
seek_t = setTimeout (function () { seek_t = 0; preview_cb (sx); }, 260);
}
no_seek = 0;
click_time = e.timeStamp;
}, false);
var is_dragging = false;
var skip = 3;
q.canvas.addEventListener ('mousemove', function ( e ) {
if (!is_dragging || !q.act_point) return ;
var ex = 0;
var ey = 0;
if (e.touches) {
if (e.touches.length > 1) { return ; }
ex = e.touches[0].clientX;
ey = e.touches[0].clientY;
} else {
ex = e.clientX;
ey = e.clientY;
}
var bounds = q.canvas.getBoundingClientRect ();
var cw = q.cw;
var ch = q.ch;
var posx = ex - bounds.left;
var posy = ey - bounds.top;
var rel_x = posx / cw;
var rel_y = posy / ch;
q.act_point.ax = posx;
q.act_point.ay = posy;
q.act_point.x = rel_x;
q.act_point.y = rel_y;
var duration;
var region = q.wv.regions.list[0];
if (region) {
duration = region.end - region.start;
} else {
duration = q.wv.getDuration();
}
q.act_point.time = duration * rel_x;
q.act_point.val = ((1 - rel_y) * (q.act.max - q.act.min)) + q.act.min;
no_seek = 1;
q.Render ();
if (--skip === 0) {
skip = 4;
_process ( q, q.wv.backend.buffer );
}
});
q.canvas.addEventListener ('mousedown', function ( e ) {
is_dragging = false;
if (!q.act) return ;
var bounds = q.canvas.getBoundingClientRect ();
var cw = q.cw;
var ch = q.ch;
var posx = e.clientX - bounds.left;
var posy = e.clientY - bounds.top;
var dist_x = e.is_touch ? 20 : 10;
var dist_y = e.is_touch ? 20 : 9;
if (!q.points[q.act.id]) q.points[q.act.id] = [];
for (var o = 0; o < q.points[q.act.id].length; ++o)
{
var curr = q.points[q.act.id][ o ];
if ( Math.abs (curr.ax - posx) < dist_x && Math.abs (curr.ay - posy) < dist_y)
{
is_dragging = true;
no_seek = 1;
q.act_point = curr;
q.Render ();
break;
}
}
if (!is_dragging)
{
q.act_point = null;
q.Render ();
}
});
q.canvas.addEventListener ('mouseup', function ( e ) {
is_dragging = false;
});
var act_el = null;
q.modal.el_body.addEventListener ('mouseover', function(e) {
if (!q.in_auto) return ;
if (e.target.tagName === 'INPUT') {
e.target.classList.add ('pk_aut');
}
});
q.modal.el_body.addEventListener ('mouseout', function(e) {
if (!q.in_auto) return ;
if (e.target.tagName === 'INPUT') {
e.target.classList.remove ('pk_aut');
}
});
q.modal.el_body.addEventListener ('click', function(e) {
if (!q.in_auto) return ;
if (e.target.classList.contains ('pk_aut'))
{
if (act_el) {
act_el.classList.remove ('pk_aut_act');
act_el = null;
}
e.target.classList.add ('pk_aut_act');
act_el = e.target;
if (!e.target.id) e.target.id = 'pk' + (++_aid);
q.act = {
id: e.target.id,
el: e.target,
type:e.target.range,
min:e.target.min/1,
max:e.target.max/1,
step:e.target.step/1
};
if (!q.points[q.act.id]) q.points[q.act.id] = [];
q.Render ();
}
// console.log( 'click ', e.target );
});
};
function _make_btn_auto ( q ) {
var btn_automate = d.createElement ('a');
btn_automate.className = 'pk_modal_a_bottom';
btn_automate.innerHTML = 'AUTOMATE';
var in_auto = false;
btn_automate.onclick = function () {
q.in_auto = !q.in_auto;
if (q.in_auto) {
btn_automate.classList.add ('pk_act');
} else {
btn_automate.classList.remove ('pk_act');
}
};
q.modal.el_body.appendChild( btn_automate );
return (btn_automate);
};
function _make_canvas ( q ) {
var cc = document.createElement ('canvas');
cc.width = 500; cc.height = 200;
cc.style.background = '#000';
var ctx = cc.getContext('2d');
q.modal.el_body.appendChild( cc );
var buff = q.wv.backend.buffer;
if (!buff) return ([cc, ctx]);
var img = new Image();
img.onload = function () {
ctx.drawImage (img, 0, 0);
if (q.waveDarken) {
ctx.fillStyle = 'rgba(0,0,0,' + q.waveDarken + ')';
ctx.fillRect (0, 0, q.cw, q.ch);
}
if (q.act) q.Render ();
};
var offset; var length;
var region = q.wv.regions.list[0];
if (region) {
offset = (region.start * buff.sampleRate) >> 0;
length = (region.end * buff.sampleRate) >> 0;
}
_process ( q, buff );
img.src = q.app.engine.GetWave (buff, 500, 200, offset, length);
return ([cc, ctx]);
};
function _compare ( a, b ) {
if (a.x > b.x) return 1;
return -1;
};
function _process ( q, buffer ) {
if (!buffer) return ;
var getOfflineAudioContext = function (channels, sampleRate, duration) {
return new (window.OfflineAudioContext ||
window.webkitOfflineAudioContext)(channels, duration, sampleRate);
};
var region = q.wv.regions.list[0];
var offs = 0;
var durr = buffer.duration;
if (region) {
offs = region.start;
durr = region.end - region.start;
}
var rate = buffer.sampleRate;
var from = Math.min (buffer.length, Math.max (0, (offs * rate) >> 0));
var to = Math.min (buffer.length, ((offs + durr) * rate) >> 0);
var len = Math.max (1, to - from);
durr = len / rate;
var audio_ctx = getOfflineAudioContext (
1, // orig_buffer.numberOfChannels,
8000,
Math.max (1, (durr * 8000) >> 0)
);
var newbuffer = audio_ctx.createBuffer (1, len, rate);
newbuffer.getChannelData ( 0 ).set (
buffer.getChannelData ( 0 ).subarray ( from, to )
);
var source = audio_ctx.createBufferSource ();
source.buffer = newbuffer;
//var fx = q.app.engine.GetFX ('Gain', q.GetValue ());
//console.log ( fx.filter ( audio_ctx, audio_ctx.destination, source ) );
source.connect (audio_ctx.destination);
source.start (0); //, offs, durr);
var offline_callback = function( rendered_buffer ) {
q.rbuff = rendered_buffer;
// var img = new Image();
// img.src = q.app.engine.GetWave (rendered_buffer, 500, 200);
q.Render ();
};
var offline_renderer = audio_ctx.startRendering();
if (offline_renderer)
offline_renderer.then( offline_callback ).catch(function() {});
else
audio_ctx.oncomplete = function ( e ) {
offline_callback ( e.renderedBuffer );
};
// ---------
};
};
PKAudioEditor._deps.FxAUT = FXAutomation;
})( window, document, PKAudioEditor );
+3123
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+635
View File
@@ -0,0 +1,635 @@
(function ( w, d, PKAE ) {
'use strict';
var StringUtils = {
readUTF16String: function(bytes, bigEndian, maxBytes) {
var ix = 0;
var offset1 = 1, offset2 = 0;
maxBytes = Math.min(maxBytes||bytes.length, bytes.length);
if( bytes[0] == 0xFE && bytes[1] == 0xFF ) {
bigEndian = true;
ix = 2;
} else if( bytes[0] == 0xFF && bytes[1] == 0xFE ) {
bigEndian = false;
ix = 2;
}
if( bigEndian ) {
offset1 = 0;
offset2 = 1;
}
var arr = [];
for( var j = 0; ix < maxBytes; j++ ) {
var byte1 = bytes[ix+offset1];
var byte2 = bytes[ix+offset2];
var word1 = (byte1<<8)+byte2;
ix += 2;
if( word1 == 0x0000 ) {
break;
} else if( byte1 < 0xD8 || byte1 >= 0xE0 ) {
arr[j] = String.fromCharCode(word1);
} else {
var byte3 = bytes[ix+offset1];
var byte4 = bytes[ix+offset2];
var word2 = (byte3<<8)+byte4;
ix += 2;
arr[j] = String.fromCharCode(word1, word2);
}
}
var string = new String(arr.join(""));
string.bytesReadCount = ix;
return string;
},
readUTF8String: function(bytes, maxBytes) {
var ix = 0;
maxBytes = Math.min(maxBytes||bytes.length, bytes.length);
if( bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF ) {
ix = 3;
}
var arr = [];
for( var j = 0; ix < maxBytes; j++ ) {
var byte1 = bytes[ix++];
if( byte1 == 0x00 ) {
break;
} else if( byte1 < 0x80 ) {
arr[j] = String.fromCharCode(byte1);
} else if( byte1 >= 0xC2 && byte1 < 0xE0 ) {
var byte2 = bytes[ix++];
arr[j] = String.fromCharCode(((byte1&0x1F)<<6) + (byte2&0x3F));
} else if( byte1 >= 0xE0 && byte1 < 0xF0 ) {
var byte2 = bytes[ix++];
var byte3 = bytes[ix++];
arr[j] = String.fromCharCode(((byte1&0xFF)<<12) + ((byte2&0x3F)<<6) + (byte3&0x3F));
} else if( byte1 >= 0xF0 && byte1 < 0xF5) {
var byte2 = bytes[ix++];
var byte3 = bytes[ix++];
var byte4 = bytes[ix++];
var codepoint = ((byte1&0x07)<<18) + ((byte2&0x3F)<<12)+ ((byte3&0x3F)<<6) + (byte4&0x3F) - 0x10000;
arr[j] = String.fromCharCode(
(codepoint>>10) + 0xD800,
(codepoint&0x3FF) + 0xDC00
);
}
}
var string = new String(arr.join(""));
string.bytesReadCount = ix;
return string;
},
readNullTerminatedString: function(bytes, maxBytes) {
var arr = [];
maxBytes = maxBytes || bytes.length;
for ( var i = 0; i < maxBytes; ) {
var byte1 = bytes[i++];
if( byte1 == 0x00 ) break;
arr[i-1] = String.fromCharCode(byte1);
}
var string = new String(arr.join(""));
string.bytesReadCount = i;
return string;
}
};
var getBytesAt = function(data, iOffset, iLength) {
if (iOffset < 0 || iLength < 0 || iOffset + iLength > data.byteLength) return [];
var bytes = new Array(iLength);
for( var i = 0; i < iLength; i++ ) {
bytes[i] = data.getUint8(iOffset+i);
}
return bytes;
};
var getStringWithCharsetAt = function(data, iOffset, iLength, iCharset) {
var bytes = getBytesAt(data, iOffset, iLength);
var sString;
switch( (iCharset || '').toString().toLowerCase() ) {
case 'utf-16':
case 'utf-16le':
case 'utf-16be':
sString = StringUtils.readUTF16String(bytes, iCharset);
break;
case 'utf-8':
sString = StringUtils.readUTF8String(bytes);
break;
default:
sString = StringUtils.readNullTerminatedString(bytes);
break;
}
return sString;
};
var ID3v2 = {
readFrameData: {}
};
var getStringAt = function(data, iOffset, iLength) {
if (iOffset < 0 || iLength < 0 || iOffset + iLength > data.byteLength) return '';
var aStr = [];
for (var i=iOffset,j=0;i<iOffset+iLength;i++,j++) {
aStr[j] = String.fromCharCode(data.getUint8(i));
}
return aStr.join("");
};
var getLongAt = function(data, iOffset, bBigEndian) {
var iByte1 = data.getUint8(iOffset),
iByte2 = data.getUint8(iOffset + 1),
iByte3 = data.getUint8(iOffset + 2),
iByte4 = data.getUint8(iOffset + 3);
var iLong = bBigEndian ?
(((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4
: (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1;
if (iLong < 0) iLong += 4294967296;
return iLong;
};
var getShortAt = function(data, iOffset, bBigEndian) {
var iShort = bBigEndian ?
(data.getUint8(iOffset) << 8) + data.getUint8(iOffset + 1)
: (data.getUint8(iOffset + 1) << 8) + data.getUint8(iOffset);
if (iShort < 0) iShort += 65536;
return iShort;
};
var getInteger24At = function(data, iOffset, bBigEndian) {
var iByte1 = data.getUint8(iOffset),
iByte2 = data.getUint8(iOffset + 1),
iByte3 = data.getUint8(iOffset + 2);
var iInteger = bBigEndian ?
((((iByte1 << 8) + iByte2) << 8) + iByte3)
: ((((iByte3 << 8) + iByte2) << 8) + iByte1);
if (iInteger < 0) iInteger += 16777216;
return iInteger;
};
var isBitSetAt = function ( dataview, offset, bit ) {
var ibyte = dataview.getUint8(offset);
return (ibyte & (1 << bit)) != 0;
};
var readSynchsafeInteger32At = function (offset, data) {
var size1 = data.getUint8(offset);
var size2 = data.getUint8(offset+1);
var size3 = data.getUint8(offset+2);
var size4 = data.getUint8(offset+3);
// 0x7f = 0b01111111
var size = size4 & 0x7f
| ((size3 & 0x7f) << 7)
| ((size2 & 0x7f) << 14)
| ((size1 & 0x7f) << 21);
return size;
};
var readFrameFlags = function(data, offset) {
return {
format: {
unsynchronisation: isBitSetAt(data, offset+1, 1),
data_length_indicator: isBitSetAt(data, offset+1, 0)
}
};
};
var _shortcuts = {
"title" : ["TIT2", "TT2"],
"artist" : ["TPE1", "TP1"],
"album" : ["TALB", "TAL"],
"year" : ["TDRC", "TYER", "TYE"],
"comment" : ["COMM", "COM"],
"track" : ["TRCK", "TRK"],
"genre" : ["TCON", "TCO"],
"picture" : ["APIC", "PIC"],
"lyrics" : ["USLT", "ULT"]
};
var _defaultShortcuts = ["title", "artist", "album", "year", "comment", "track", "genre", "picture", "lyrics"];
var getTagsFromShortcuts = function(shortcuts) {
var tags = [];
for( var i = 0, shortcut; shortcut = shortcuts[i]; i++ ) {
tags = tags.concat(_shortcuts[shortcut]||[shortcut]);
}
return tags;
};
var getFrameData = function( frames, ids ) {
if( typeof ids == 'string' ) { ids = [ids]; }
for( var i = 0, id; id = ids[i]; i++ ) {
if( id in frames ) { return frames[id]; }
}
};
var readFrames = function (offset, end, data, id3header, tags) {
var frames = {};
var major = id3header["major"];
tags = getTagsFromShortcuts(tags || _defaultShortcuts);
end = Math.min(end, data.byteLength);
while( offset < end ) {
var readFrameFunc = null;
var frameData = data;
var frameDataOffset = offset;
var flags = null;
switch( major ) {
case 2:
if (frameDataOffset + 6 > end) return frames;
var frameID = getStringAt(frameData, frameDataOffset, 3);
var frameSize = getInteger24At(frameData, frameDataOffset+3, true);
var frameHeaderSize = 6;
break;
case 3:
if (frameDataOffset + 10 > end) return frames;
var frameID = getStringAt(frameData, frameDataOffset, 4);
var frameSize = getLongAt(frameData, frameDataOffset+4, true);
var frameHeaderSize = 10;
break;
case 4:
if (frameDataOffset + 10 > end) return frames;
var frameID = getStringAt(frameData, frameDataOffset, 4);
var frameSize = readSynchsafeInteger32At(frameDataOffset+4, frameData);
var frameHeaderSize = 10;
break;
}
// if last frame GTFO
if( frameID == "" ) { break; }
if( !frameSize || frameDataOffset + frameHeaderSize + frameSize > end ) { break; }
// advance data offset to the next frame data
offset += frameHeaderSize + frameSize;
// skip unwanted tags
if( tags.indexOf( frameID ) < 0 ) { continue; }
// read frame message and format flags
if( major > 2 )
{
flags = readFrameFlags(frameData, frameDataOffset+8);
}
frameDataOffset += frameHeaderSize;
// the first 4 bytes are the real data size
// (after unsynchronisation && encryption)
if( flags && flags.format.data_length_indicator )
{
frameDataOffset += 4;
frameSize -= 4;
}
// TODO: support unsynchronisation
if( flags && flags.format.unsynchronisation )
{
//frameData = removeUnsynchronisation(frameData, frameSize);
continue;
}
// find frame parsing function
if( frameID in ID3v2.readFrameData ) {
readFrameFunc = ID3v2.readFrameData[frameID];
} else if( frameID[0] == "T" ) {
readFrameFunc = ID3v2.readFrameData["T*"];
}
var parsedData = readFrameFunc ? readFrameFunc(frameDataOffset, frameSize, frameData, flags) : undefined;
if( !(frameID in frames) ) {
frames[frameID] = parsedData;
}
}
return frames;
};
function getTextEncoding( bite ) {
var charset;
switch( bite )
{
case 0x00:
charset = 'iso-8859-1';
break;
case 0x01:
charset = 'utf-16';
break;
case 0x02:
charset = 'utf-16be';
break;
case 0x03:
charset = 'utf-8';
break;
}
return charset;
}
ID3v2.readFrameData['APIC'] = function readPictureFrame(offset, length, data, flags, v) {
v = v || '3';
var start = offset;
var charset = getTextEncoding( data.getUint8(offset) );
switch( v ) {
case '2':
var format = getStringAt(data, offset+1, 3);
offset += 4;
break;
case '3':
case '4':
var format = getStringWithCharsetAt(data, offset+1, length - (offset-start), '');
offset += 1 + format.bytesReadCount;
break;
}
var bite = data.getUint8(offset, 1);
var desc = getStringWithCharsetAt(data, offset+1, length - (offset-start), charset);
offset += 1 + desc.bytesReadCount;
return {
"format" : format.toString(),
"type" : bite,
"description" : desc.toString(),
"data" : getBytesAt(data, offset, (start+length) - offset)
};
};
ID3v2.readFrameData['COMM'] = function readCommentsFrame(offset, length, data) {
var start = offset;
var charset = getTextEncoding( data.getUint8(offset) );
var language = getStringAt(data, offset+1, 3 );
var shortdesc = getStringWithCharsetAt(data, offset+4, length-4, charset);
offset += 4 + shortdesc.bytesReadCount;
var text = getStringWithCharsetAt(data, offset, (start+length) - offset, charset );
return {
language : language,
short_description : shortdesc.toString(),
text : text.toString()
};
};
ID3v2.readFrameData['COM'] = ID3v2.readFrameData['COMM'];
ID3v2.readFrameData['PIC'] = function(offset, length, data, flags) {
return ID3v2.readFrameData['APIC'](offset, length, data, flags, '2');
};
ID3v2.readFrameData['T*'] = function readTextFrame(offset, length, data) {
var charset = getTextEncoding( data.getUint8(offset) );
return getStringWithCharsetAt(data, offset+1, length-1, charset).toString();
};
ID3v2.readFrameData['TCON'] = function readGenreFrame(offset, length, data) {
var text = ID3v2.readFrameData['T*'].apply( this, arguments );
return text.replace(/^\(\d+\)/, '');
};
ID3v2.readFrameData['TCO'] = ID3v2.readFrameData['TCON'];
ID3v2.readFrameData['USLT'] = function readLyricsFrame(offset, length, data) {
var start = offset;
var charset = getTextEncoding( data.getUint8(offset) );
var language = getStringAt(data, offset+1, 3 );
var descriptor = getStringWithCharsetAt(data, offset+4, length-4, charset );
offset += 4 + descriptor.bytesReadCount;
var lyrics = getStringWithCharsetAt(data, offset, (start+length) - offset, charset );
return {
language : language,
descriptor : descriptor.toString(),
lyrics : lyrics.toString()
};
};
ID3v2.readFrameData['ULT'] = ID3v2.readFrameData['USLT'];
ID3v2.ReadTags = function ( arraybuffer ) {
if (!arraybuffer || arraybuffer.byteLength < 10) return null;
var data = new DataView ( arraybuffer );
var offset = 0;
if (getStringAt(data, 0, 3) !== 'ID3') return null;
var major = data.getUint8(offset+3);
if( major < 2 || major > 4 ) { return null; }
var unsynch = isBitSetAt(data, offset+5, 7);
var xheader = isBitSetAt(data, offset+5, 6);
var size = readSynchsafeInteger32At(offset+6, data);
var end = offset + 10 + size;
if (end > data.byteLength) return null;
offset += 10;
if( xheader ) {
if (offset + 4 > end) return null;
var xheadersize = data.getInt32( offset, true ); //data.getLongAt(offset, true);
// The 'Extended header size', currently 6 or 10 bytes, excludes itself.
offset += xheadersize + 4;
if (offset > end) return null;
}
var id3 = {};
var frames = unsynch ? {} : readFrames(offset, end, data, { major: major });
// create shortcuts for most common data
for( var name in _shortcuts ) if(_shortcuts.hasOwnProperty(name)) {
var data = getFrameData( frames, _shortcuts[name] );
if( data ) id3[name] = data;
}
return id3;
};
var _s = function (s) {
for (var a = [], i = 0; i < s.length; ++i) a[i] = s.charCodeAt(i) & 255;
return a;
};
var _n = function (n) {
return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255];
};
var _ss = function (n) {
return [(n >>> 21) & 127, (n >>> 14) & 127, (n >>> 7) & 127, n & 127];
};
var _u = function (s, bom) {
s = (s || '').toString();
for (var a = bom ? [255, 254] : [], i = 0, c; i < s.length; ++i) {
c = s.charCodeAt(i); a.push(c & 255, c >> 8);
}
return a;
};
var _v = function (v, k) {
return (v && v[k] !== undefined) ? v[k] : (v || '');
};
var _f = function (id, b) {
return b && b.length ? _s(id).concat(_n(b.length), [0, 0], b) : [];
};
var _at = function (ab) {
var b = new Uint8Array(ab);
if (b.length < 10 || b[0] != 73 || b[1] != 68 || b[2] != 51) return 0;
var n = 10 + ((b[6] & 127) << 21 | (b[7] & 127) << 14 | (b[8] & 127) << 7 | (b[9] & 127)) + ((b[5] & 16) ? 10 : 0);
return n > b.length ? 0 : n;
};
var _apic = function (p) {
if (!p || !p.data || !p.data.length) return [];
var d = p.data, a = [0].concat(_s(p.format || 'image/jpeg'), [0, p.type || 3, 0]);
for (var i = 0; i < d.length; ++i) a.push(d[i]);
return a;
};
var _txt = function (s) {
s = (s || '').toString();
return s ? [1].concat(_u(s, 1)) : [];
};
ID3v2.WriteTags = function (ab, tag) {
var fr = [], p = tag.picture;
fr = fr.concat(
_f('TIT2', _txt(tag.title)),
_f('TPE1', _txt(tag.artist)),
_f('TALB', _txt(tag.album)),
_f('TYER', _txt(tag.year)),
_f('TCON', _txt(tag.genre)),
_f('TRCK', _txt(tag.track)),
_f('COMM', _v(tag.comment, 'text') ? [1].concat(_s('eng'), _u('', 1), [0, 0], _u(_v(tag.comment, 'text'), 1)) : []),
_f('USLT', _v(tag.lyrics, 'lyrics') ? [1].concat(_s('eng'), _u('', 1), [0, 0], _u(_v(tag.lyrics, 'lyrics'), 1)) : []),
_f('APIC', _apic(p))
);
var hd = _s('ID3').concat([3, 0, 0], _ss(fr.length));
var au = new Uint8Array(ab, _at(ab));
var out = new Uint8Array(hd.length + fr.length + au.length), o = 0;
out.set(hd, o); o += hd.length;
out.set(fr, o); o += fr.length;
out.set(au, o);
return out.buffer;
};
w.ID3v2 = ID3v2;
/// -------
var ID4 = {};
ID4.types = {
'0' : 'uint8',
'1' : 'text',
'13' : 'jpeg',
'14' : 'png',
'21' : 'uint8'
};
ID4.atom = {
'©alb': ['album'],
'©art': ['artist'],
'©ART': ['artist'],
'aART': ['artist'],
'©day': ['year'],
'©nam': ['title'],
'©gen': ['genre'],
'trkn': ['track'],
'covr': ['picture'],
'©lyr': ['lyrics'],
'©cmt': ['comment']
};
ID4.ReadTags = function(arraybuffer) {
if (!arraybuffer || arraybuffer.byteLength < 8) return null;
var data = new DataView ( arraybuffer );
var tag = {};
readAtom(tag, data, 0, data.byteLength);
return tag;
};
function readAtom(tag, data, offset, length, indent)
{
indent = indent === undefined ? "" : indent + " ";
var seek = offset;
var end = Math.min(offset + length, data.byteLength);
while (seek + 8 <= end)
{
var atomSize = data.getInt32(seek); // getLongAt(data, seek, true);
if (atomSize == 0) return;
if (atomSize < 8 || seek + atomSize > end) return;
var atomName = getStringAt(data, seek + 4, 4);
// Container atoms
if (atomName === 'meta')
{
seek += 4; // next_item_id (uint32)
readAtom(tag, data, seek + 8, atomSize - 8, indent);
return;
}
if (atomName === 'moov' || atomName === 'udta' || atomName === 'ilst' ) // ['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1)
{
readAtom(tag, data, seek + 8, atomSize - 8, indent);
return;
}
/*
if (['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1)
{
if (atomName === 'meta') seek += 4; // next_item_id (uint32)
readAtom(tag, data, seek + 8, atomSize - 8, indent);
return;
}
*/
// Value atoms
if (ID4.atom[atomName])
{
if (seek + 24 > end) return;
var klass = getInteger24At(data, seek + 16 + 1, true);
var atom = ID4.atom[atomName];
var type = ID4.types[klass];
if (atomName === 'trkn')
{
if (seek + 29 > end) return;
tag[atom[0]] = data.getUint8(seek + 16 + 11);
tag['count'] = data.getUint8(seek + 16 + 13);
}
else
{
// 16: name + size + "data" + size (4 bytes each)
// 4: atom version (1 byte) + atom flags (3 bytes)
// 4: NULL (usually locale indicator)
var dataStart = seek + 16 + 4 + 4;
var dataEnd = atomSize - 16 - 4 - 4;
if (dataEnd < 0 || dataStart + dataEnd > end) return;
var atomData;
switch( type ) {
case 'text':
atomData = getStringWithCharsetAt(data, dataStart, dataEnd, "UTF-8");
break;
case 'uint8':
atomData = getShortAt(data, dataStart);
break;
case 'jpeg':
case 'png':
atomData = {
format : "image/" + type,
data : getBytesAt(data, dataStart, dataEnd)
};
break;
}
if (atom[0] === "comment") {
tag[atom[0]] = {
"text": atomData
};
} else {
tag[atom[0]] = atomData;
}
}
}
seek += atomSize;
}
}
w.ID4 = ID4;
})( window, document, PKAudioEditor );
+67
View File
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en" manifest="audiomass.appcache">
<head>
<title>AudioMass - Audio Editor</title>
<link href="ico.png" rel="shortcut icon">
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"/>
<meta name="description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool"/>
<meta property="og:image" content="https://audiomass.co/icon.jpg"/>
<meta property="og:title" content="AudioMass">
<meta property="og:url" content="https://audiomass.co/">
<meta property="og:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="keywords" content="AudioMass, WebAudio, WaveForm, audio editing, free audio editing, audio tool, waveform editor, sound editor, open source">
<link rel="apple-touch-icon" href="https://audiomass.co/icon-app.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="AudioMass">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@pkalogiros">
<meta name="twitter:creator" content="@pkalogiros">
<meta name="twitter:title" content="AudioMass - Audio Editor">
<meta name="twitter:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="twitter:image" content="https://audiomass.co/icon.jpg">
<link rel="stylesheet" type="text/css" href="all.css">
</head>
<body>
<div id="app"></div>
<script src="all.build.js"></script>
<script>
var editor = PKAudioEditor.init ('app');
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
try {
var rl = 0;
function nr ( sw ) {
if (document.getElementById ('pk_upd')) return ;
var el = document.createElement ('div');
el.id = 'pk_upd';
el.className = 'pk_oneup';
el.style.cssText = 'top:58px;margin-top:0;opacity:1;transform:translateX(-50%);pointer-events:auto;font-size:13px';
el.innerHTML = 'New version ready <button class="pk_modal_a_bottom pk_modal_a_accpt" style="float:none;display:inline-block;margin-left:10px">Reload</button>';
document.body.appendChild ( el );
el.getElementsByTagName ('button')[0].onclick = function () {
rl = 1;
sw && sw.postMessage ('SKIP_WAITING');
setTimeout (function () { location.reload (); }, 900);
};
}
navigator.serviceWorker.addEventListener ('controllerchange', function () { rl && location.reload (); });
navigator.serviceWorker.register( 'sw.js' ).then (function (r) {
function ch ( sw ) { sw && (sw.state === 'installed' && navigator.serviceWorker.controller ? nr (sw) : sw.onstatechange = function () { sw.state === 'installed' && navigator.serviceWorker.controller && nr (sw); }); }
r.waiting && navigator.serviceWorker.controller && nr (r.waiting);
ch (r.installing);
r.onupdatefound = function () { ch (r.installing); };
});
} catch ( error ) {}
});
}
</script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>AudioMass - Audio Editor</title>
<link href="ico.png" rel="shortcut icon">
<meta charset="utf-8" />
<link rel="manifest" href="manifest.json">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool"/>
<meta property="og:image" content="https://audiomass.co/icon.jpg"/>
<meta property="og:title" content="AudioMass">
<meta property="og:url" content="https://audiomass.co/">
<meta property="og:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="keywords" content="AudioMass, WebAudio, WaveForm, audio editing, free audio editing, audio tool, waveform editor, sound editor, open source">
<link rel="apple-touch-icon" href="https://audiomass.co/icon-app.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="AudioMass">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@pkalogiros">
<meta name="twitter:creator" content="@pkalogiros">
<meta name="twitter:title" content="AudioMass - Audio Editor">
<meta name="twitter:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="twitter:image" content="https://audiomass.co/icon.jpg">
<link rel="stylesheet" type="text/css" href="all.css">
</head>
<body>
<div id="app"></div>
<script src="all.build.js"></script>
<script>
var editor = PKAudioEditor.init ('app');
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
try {
var rl = 0;
function nr ( sw ) {
if (document.getElementById ('pk_upd')) return ;
var el = document.createElement ('div');
el.id = 'pk_upd';
el.className = 'pk_oneup';
el.style.cssText = 'top:58px;margin-top:0;opacity:1;transform:translateX(-50%);pointer-events:auto;font-size:13px';
el.innerHTML = 'New version ready <button class="pk_modal_a_bottom pk_modal_a_accpt" style="float:none;display:inline-block;margin-left:10px">Reload</button>';
document.body.appendChild ( el );
el.getElementsByTagName ('button')[0].onclick = function () {
rl = 1;
sw && sw.postMessage ('SKIP_WAITING');
setTimeout (function () { location.reload (); }, 900);
};
}
navigator.serviceWorker.addEventListener ('controllerchange', function () { rl && location.reload (); });
navigator.serviceWorker.register( 'sw.js' ).then (function (r) {
function ch ( sw ) { sw && (sw.state === 'installed' && navigator.serviceWorker.controller ? nr (sw) : sw.onstatechange = function () { sw.state === 'installed' && navigator.serviceWorker.controller && nr (sw); }); }
r.waiting && navigator.serviceWorker.controller && nr (r.waiting);
ch (r.installing);
r.onupdatefound = function () { ch (r.installing); };
});
} catch ( error ) {}
});
}
</script>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>AudioMass - Audio Editor</title>
<link href="ico.png" rel="shortcut icon">
<meta charset="utf-8" />
<link rel="manifest" href="manifest.json">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool"/>
<meta property="og:image" content="https://audiomass.co/icon.jpg"/>
<meta property="og:title" content="AudioMass">
<meta property="og:url" content="https://audiomass.co/">
<meta property="og:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="keywords" content="AudioMass, WebAudio, WaveForm, audio editing, free audio editing, audio tool, waveform editor, sound editor, open source">
<link rel="apple-touch-icon" href="https://audiomass.co/icon-app.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="AudioMass">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@pkalogiros">
<meta name="twitter:creator" content="@pkalogiros">
<meta name="twitter:title" content="AudioMass - Audio Editor">
<meta name="twitter:description" content="AudioMass is a free full-featured web-based audio &amp; waveform editing tool">
<meta name="twitter:image" content="https://audiomass.co/icon.jpg">
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<div id="app"></div>
<script src="dist/wavesurfer.js"></script>
<script src="dist/plugin/wavesurfer.regions.js"></script>
<script src="oneup.js"></script>
<script src="app.js"></script>
<script src="keys.js"></script>
<script src="markers.js"></script>
<script src="contextmenu.js"></script>
<script src="lufs.js"></script>
<script src="ui-fx.js"></script>
<script src="ui.js"></script>
<script src="modal.js"></script>
<script src="state.js"></script>
<script src="engine.js"></script>
<script src="actions.js"></script>
<script src="drag.js"></script>
<script src="recorder.js"></script>
<script src="welcome.js"></script>
<script src="fx-pg-eq.js"></script>
<script src="fx-auto.js"></script>
<script src="local.js"></script>
<script src="id3.js"></script>
<script src="lzma.js"></script>
<script src="amss-format.js"></script>
<script src="multitrack.js"></script>
<script>
var editor = PKAudioEditor.init ('app');
if ('serviceWorker' in navigator) {
try {
// navigator.serviceWorker.register( 'sw.js' );
} catch ( error ) {}
}
</script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
(function( w, d, PKAE ) {
'use strict';
function KeyHandler () {
var q = this;
q.keyMap = {}; // holds a map of all the active keys
q.callbacks = {}; // callbacks for when a key combintation becomes active
q.singleCallbacks = {}; // callbacks to the 'keypress' event - not required
q.mac = /mac|iphone|ipad|ipod/.test ((navigator.platform || '').toLowerCase ());
q.isAccel = function ( e ) {
return e && (q.mac ? e.metaKey : e.ctrlKey) && !e.altKey;
};
q.isEditTarget = function ( e ) {
var t = e && e.target;
return !!(t && /INPUT|TEXTAREA|SELECT/.test (t.tagName));
};
q.addCallback = function (callback_name, callback_function, keys) {
q.callbacks[ callback_name ] = {
keys : keys,
callback : callback_function
};
};
q.addSingleCallback = function (callback_name, callback_function, key) {
q.singleCallbacks[ callback_name ] = {
key : key,
callback : callback_function
};
};
q.removeCallback = function ( callback_name ) {
q.callbacks[ callback_name ] = null;
};
d.addEventListener ('keydown', function ( e ) {
var keyCode = e.keyCode;
q.keyDown (keyCode, e);
});
q.keyDown = function (keyCode, e ) {
q.keyMap[keyCode] = 1;
for (var key in q.callbacks) {
var group = q.callbacks[key];
if (!group) continue;
if (keyCode !== group.keys[group.keys.length - 1]) continue;
var l = group.keys.length;
var all_ok = true;
while (l-- > 0) {
if (!q.keyMap[group.keys[l]])
{
all_ok = false;
break;
}
}
all_ok && group.callback && group.callback ( keyCode, q.keyMap, e );
}
};
q.keyUp = function ( keyCode ) {
q.keyMap[keyCode] = 0;
};
q.keyPress = function ( keyCode, e ) {
if (q.isEditTarget ( e )) return ;
for (var key in q.singleCallbacks) {
var group = q.singleCallbacks[key];
if (!group) continue;
if (group.key === keyCode)
group.callback && group.callback ( e );
}
};
d.addEventListener ('keyup', function ( e ) {
var keyCode = e.keyCode;
q.keyUp (keyCode);
});
d.addEventListener ('keypress', function ( e ) {
var keyCode = e.keyCode;
q.keyPress (keyCode, e);
});
w.addEventListener ('blur', function ( e ) {
q.keyMap = {};
}, false);
d.addEventListener ('contextmenu', function( e ) {
e.preventDefault();
}, false);
};
PKAE._deps.keyhandler = KeyHandler;
})( window, document, PKAudioEditor );
+15591
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
+264
View File
@@ -0,0 +1,264 @@
(function ( w, d, PKAE ) {
'use strict';
var db;
var db_name = 'audiomass';
var db_version = 1;
var db_ready = false;
var compressors = {
'l4z' : {
ready: false,
loading: false,
compress: null,
decompress: null,
init : function ( callback ) {
var q = this;
q.loading = true;
var lz4BlockWASM;
lz4BlockCodec.createInstance('wasm').then(instance => {
lz4BlockWASM = instance;
q.ready = true;
q.loading = false;
q.compress = function( input, offset ) {
if (!lz4BlockWASM) {
if (input instanceof ArrayBuffer)
return new Uint8Array(input);
else
return input;
}
return lz4BlockWASM.encodeBlock(input, 0);
};
q.decompress = function( input, offset, size ) {
if (!lz4BlockWASM) {
if (input instanceof ArrayBuffer)
return new Uint8Array(input);
else
return input;
}
return lz4BlockWASM.decodeBlock(input, 0, size);
};
callback && callback ();
});
// ---
}
}
};
var compression = 'l4z';
function SaveLocal ( app ) {
var q = this;
q.on = false;
this.Init = function ( callback ) {
if (q.on) {
callback && callback ();
return ;
}
if (!window.indexedDB) {
callback && callback ('err');
return ;
}
var request = indexedDB.open (db_name, db_version);
request.onerror = function(e) {
callback && callback ('err');
// console.error('Unable to open database.');
};
request.onupgradeneeded = function(e) {
var db = e.target.result;
db.createObjectStore('sessions', {keyPath:'id'});
};
request.onsuccess = function(e) {
db = e.target.result;
db.onerror = function( e ) {
console.log( e );
};
setTimeout(function() {
db_ready = true;
q.on = true;
callback && callback ();
app.fireEvent ('DidOpenDB', q);
},120);
};
};
this.SaveSession = function ( buffer, id, name, callback, quiet ) {
var q = this;
var comp = compressors[ compression ];
if (!comp.loading && !comp.ready) {
comp.init (function() {
q.SaveSession (buffer, id, name, callback, quiet);
});
return ;
}
var chans = buffer.numberOfChannels;
var arr_buffs = [];
var arr_buffs2 = [];
var arr;
var tmp;
var sample_rate = buffer.sampleRate;
for (var i = 0; i < chans; ++i) {
arr = buffer.getChannelData ( i );
arr_buffs2.push (arr.buffer.byteLength);
tmp = comp.compress ( arr.buffer, 0);
arr_buffs.push ( tmp.buffer.slice (tmp.byteOffset, tmp.byteLength + tmp.byteOffset));
}
tmp = null;
var ob = {
id : id,
name: name,
created: new Date().getTime(),
data: arr_buffs,
data2: arr_buffs2,
durr: buffer.duration.toFixed(3)/1,
chans: chans,
comp: compression,
thumb: PKAudioEditor.engine.GetWave (buffer),
samplerate: sample_rate,
markers: app.mrk ? app.mrk.serEd () : []
};
var trans = db.transaction(['sessions'], 'readwrite');
var addReq = trans.objectStore('sessions').add(ob);
addReq.onerror = function(e) {
app.fireEvent ('ErrorDB', e);
console.log('error storing data');
console.error(e);
};
trans.oncomplete = function ( e ) {
if (!quiet) app.fireEvent ('DidStoreDB', ob, e );
callback && callback (ob, e);
// console.log( 'data stored', id, e );
};
};
this.GetSession = function ( id, callback ) {
var trans = db.transaction(['sessions'], 'readonly');
//hard coded id
var req = trans.objectStore('sessions').get(id);
req.onsuccess = function(e) {
// console.log( e.target.result );
var record = e.target.result;
if (record && record.comp)
{
var comp = compressors[ compression ];
if (!comp.loading && !comp.ready) {
comp.init (function() {
var data_arr = [];
var tmp = null;
for (var i = 0; i < record.data.length; ++i) {
tmp = comp.decompress (record.data[i], 0, record.data2[i]);
data_arr.push (
tmp.buffer.slice (tmp.byteOffset, tmp.byteLength + tmp.byteOffset)
);
}
tmp = null;
record.data = data_arr;
callback && callback ( record );
});
return ;
}
var data_arr = [];
var tmp = null;
for (var i = 0; i < record.data.length; ++i) {
tmp = comp.decompress (record.data[i], 0, record.data2[i]);
data_arr.push (
tmp.buffer.slice (tmp.byteOffset, tmp.byteLength + tmp.byteOffset)
);
}
tmp = null;
record.data = data_arr;
}
callback && callback ( record );
};
};
this.DelSession = function ( id, callback ) {
var trans = db.transaction(['sessions'], 'readwrite');
var req = trans.objectStore('sessions').delete (id);
req.onsuccess = function (e) {
callback && callback ( id );
};
};
this.ListSessions = function ( callback ) {
var trans = db.transaction(['sessions'], 'readonly');
var object_store = trans.objectStore('sessions');
var req = object_store.openCursor();
var ret = [];
req.onerror = function(event) {
console.err("error fetching data");
};
req.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
var key = cursor.primaryKey;
var value = cursor.value;
ret.push (value);
cursor.continue();
}
else {
var rr = ret.sort(function compare( a, b ) {
if ( a.created > b.created ){
return -1;
}
if ( a.created < b.created ){
return 1;
}
return 0;
});
callback && callback (rr);
// no more results
}
};
};
// ---
};
PKAudioEditor._deps.fls = SaveLocal;
})( window, document, PKAudioEditor );
+240
View File
@@ -0,0 +1,240 @@
(function (PKAE) {
'use strict';
var C = {
block: 0.400,
hop: 0.100,
absGate: -70,
relGate: -10,
offset: -0.691,
truePeakSteps: 4
};
function db (v) {
return v > 0 ? 20 * Math.log (v) / Math.LN10 : -120;
}
function loud (v) {
return v > 0 ? C.offset + 10 * Math.log (v) / Math.LN10 : -Infinity;
}
function norm (b0, b1, b2, a0, a1, a2) {
return {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0
};
}
function highShelf (rate) {
var f0 = Math.min (1681.974450955533, rate * 0.45);
var q = 0.7071752369554196;
var gain = 3.999843853973347;
var a = Math.pow (10, gain / 40);
var w0 = 2 * Math.PI * f0 / rate;
var sn = Math.sin (w0);
var cs = Math.cos (w0);
var alpha = sn / (2 * q);
var sa = Math.sqrt (a);
return norm (
a * ((a + 1) + (a - 1) * cs + 2 * sa * alpha),
-2 * a * ((a - 1) + (a + 1) * cs),
a * ((a + 1) + (a - 1) * cs - 2 * sa * alpha),
(a + 1) - (a - 1) * cs + 2 * sa * alpha,
2 * ((a - 1) - (a + 1) * cs),
(a + 1) - (a - 1) * cs - 2 * sa * alpha
);
}
function highPass (rate) {
var f0 = Math.min (38.13547087602444, rate * 0.45);
var q = 0.5003270373238773;
var w0 = 2 * Math.PI * f0 / rate;
var sn = Math.sin (w0);
var cs = Math.cos (w0);
var alpha = sn / (2 * q);
return norm (
(1 + cs) / 2,
-(1 + cs),
(1 + cs) / 2,
1 + alpha,
-2 * cs,
1 - alpha
);
}
function kWeightCoeffs (rate) {
return [highShelf (rate), highPass (rate)];
}
function biquad (x, c, s) {
var y = c.b0 * x + c.b1 * s.x1 + c.b2 * s.x2 - c.a1 * s.y1 - c.a2 * s.y2;
s.x2 = s.x1; s.x1 = x;
s.y2 = s.y1; s.y1 = y;
return (y);
}
function interp (a, b, c, d, t) {
var t2 = t * t;
return 0.5 * ((2 * b) + (-a + c) * t +
(2 * a - 5 * b + 4 * c - d) * t2 +
(-a + 3 * b - 3 * c + d) * t2 * t);
}
function analyze (buffer) {
var rate = buffer.sampleRate;
var len = buffer.length;
var channels = buffer.numberOfChannels;
var hop = Math.max (1, (rate * C.hop) >> 0);
var block = Math.max (1, hop * 4);
if (len < block) {
block = len;
hop = len || 1;
}
var blocks = len <= block ? 1 : (((len - block) / hop) >> 0) + 1;
var sums = new Float64Array (blocks);
var coeffs = kWeightCoeffs (rate);
var slots = Math.max (1, (block / hop) >> 0);
var total = 0;
var peak = 0;
var tpeak = 0;
for (var ch = 0; ch < channels; ++ch) {
var data = buffer.getChannelData (ch);
var weight = ch > 2 ? 1.41 : 1.0;
var idx = [];
var acc = [];
var s1 = {x1:0, x2:0, y1:0, y2:0};
var s2 = {x1:0, x2:0, y1:0, y2:0};
var next = 0;
var bi = 0;
for (var z = 0; z < slots; ++z) {
idx[z] = -1;
acc[z] = 0;
}
function flush (slot) {
if (idx[slot] >= 0) {
sums[idx[slot]] += acc[slot] * weight;
idx[slot] = -1;
acc[slot] = 0;
}
}
for (var i = 0; i < len; ++i) {
if (i === next) {
var slot = bi % slots;
flush (slot);
if (bi < blocks) {
idx[slot] = bi;
acc[slot] = 0;
}
++bi;
next += hop;
}
var x = data[i];
var ax = Math.abs (x);
var y = biquad (biquad (x, coeffs[0], s1), coeffs[1], s2);
var yy = y * y;
total += x * x;
if (ax > peak) peak = ax;
if (ax > tpeak) tpeak = ax;
if (i < len - 1) {
var x0 = i ? data[i - 1] : x;
var x2 = data[i + 1];
var x3 = i < len - 2 ? data[i + 2] : x2;
for (var tp = 1; tp < C.truePeakSteps; ++tp) {
ax = Math.abs (interp (x0, x, x2, x3, tp / C.truePeakSteps));
if (ax > tpeak) tpeak = ax;
}
}
for (var j = 0; j < slots; ++j) {
if (idx[j] >= 0) acc[j] += yy;
}
}
for (var k = 0; k < slots; ++k) flush (k);
}
var energies = [];
var sum = 0;
for (var n = 0; n < blocks; ++n) {
var e = sums[n] / block;
if (loud (e) >= C.absGate) {
energies.push (e);
sum += e;
}
}
var lufs = -Infinity;
if (energies.length) {
var gate = loud (sum / energies.length) + C.relGate;
sum = 0;
var count = 0;
for (var m = 0; m < energies.length; ++m) {
if (loud (energies[m]) >= gate) {
sum += energies[m];
++count;
}
}
if (count) lufs = loud (sum / count);
}
var rms = Math.sqrt (total / Math.max (1, len * channels));
return {
lufs: lufs,
rms: rms,
rmsDb: db (rms),
peak: peak,
peakDb: db (peak),
truePeak: tpeak,
truePeakDb: db (tpeak),
blocks: blocks
};
}
function integratedLUFS (buffer) {
return analyze (buffer).lufs;
}
function gainForTarget (report, target, ceiling) {
target = target / 1;
ceiling = ceiling / 1;
if (!isFinite (ceiling)) ceiling = -1;
var gainDb = isFinite (report.lufs) ? target - report.lufs : 0;
var maxGain = ceiling - report.truePeakDb;
var limited = false;
if (gainDb > maxGain) {
gainDb = maxGain;
limited = true;
}
return {
gain: Math.pow (10, gainDb / 20),
gainDb: gainDb,
limited: limited,
expectedLUFS: isFinite (report.lufs) ? report.lufs + gainDb : report.lufs,
expectedTruePeakDb: report.truePeakDb + gainDb
};
}
PKAE._deps.lufs = {
BS1770Block: C,
kWeightCoeffs: kWeightCoeffs,
integratedLUFS: integratedLUFS,
analyze: analyze,
gainForTarget: gainForTarget,
db: db
};
})(PKAudioEditor);
+194
View File
@@ -0,0 +1,194 @@
/*******************************************************************************
lz4-block-codec-wasm.js
A javascript wrapper around a WebAssembly implementation of
LZ4 block format codec.
Copyright (C) 2018 Raymond Hill
BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Home: https://github.com/gorhill/lz4-wasm
I used the same license as the one picked by creator of LZ4 out of respect
for his creation, see https://lz4.github.io/lz4/
*/
/* global WebAssembly */
'use strict';
/******************************************************************************/
(function(context) { // >>>> Start of private namespace
/******************************************************************************/
let wd = (function() {
let url = document.currentScript.src;
let match = /[^\/]+$/.exec(url);
return match !== null ?
url.slice(0, match.index) :
'';
})();
let growMemoryTo = function(wasmInstance, byteLength) {
let lz4api = wasmInstance.exports;
let neededByteLength = lz4api.getLinearMemoryOffset() + byteLength;
let pageCountBefore = lz4api.memory.buffer.byteLength >>> 16;
let pageCountAfter = (neededByteLength + 65535) >>> 16;
if ( pageCountAfter > pageCountBefore ) {
lz4api.memory.grow(pageCountAfter - pageCountBefore);
}
return lz4api.memory.buffer;
};
let encodeBlock = function(wasmInstance, inputArray, outputOffset) {
let lz4api = wasmInstance.exports;
let mem0 = lz4api.getLinearMemoryOffset();
let hashTableSize = 65536 * 4;
let inputSize = inputArray.byteLength;
if ( inputSize >= 0x7E000000 ) { throw new RangeError(); }
let memSize =
hashTableSize +
inputSize +
outputOffset + lz4api.lz4BlockEncodeBound(inputSize);
let memBuffer = growMemoryTo(wasmInstance, memSize);
let hashTable = new Int32Array(memBuffer, mem0, 65536);
hashTable.fill(-65536, 0, 65536);
let inputMem = new Uint8Array(memBuffer, mem0 + hashTableSize, inputSize);
inputMem.set(inputArray);
let outputSize = lz4api.lz4BlockEncode(
mem0 + hashTableSize,
inputSize,
mem0 + hashTableSize + inputSize + outputOffset
);
if ( outputSize === 0 ) { return; }
let outputArray = new Uint8Array(
memBuffer,
mem0 + hashTableSize + inputSize,
outputOffset + outputSize
);
return outputArray;
};
let decodeBlock = function(wasmInstance, inputArray, inputOffset, outputSize) {
let inputSize = inputArray.byteLength;
let lz4api = wasmInstance.exports;
let mem0 = lz4api.getLinearMemoryOffset();
let memSize = inputSize + outputSize;
let memBuffer = growMemoryTo(wasmInstance, memSize);
let inputArea = new Uint8Array(memBuffer, mem0, inputSize);
inputArea.set(inputArray);
outputSize = lz4api.lz4BlockDecode(
mem0 + inputOffset,
inputSize - inputOffset,
mem0 + inputSize
);
if ( outputSize === 0 ) { return; }
return new Uint8Array(memBuffer, mem0 + inputSize, outputSize);
};
/******************************************************************************/
context.LZ4BlockWASM = function() {
this.lz4wasmInstance = undefined;
};
context.LZ4BlockWASM.prototype = {
flavor: 'wasm',
init: function() {
if (
typeof WebAssembly !== 'object' ||
typeof WebAssembly.instantiateStreaming !== 'function'
) {
this.lz4wasmInstance = null;
}
if ( this.lz4wasmInstance === null ) {
return Promise.reject();
}
if ( this.lz4wasmInstance instanceof WebAssembly.Instance ) {
return Promise.resolve(this.lz4wasmInstance);
}
if ( this.lz4wasmInstance === undefined ) {
this.lz4wasmInstance = WebAssembly.instantiateStreaming(
fetch(wd + 'lz4-block-codec.wasm', { mode: 'same-origin' })
).then(result => {
this.lz4wasmInstance = undefined;
this.lz4wasmInstance = result && result.instance || null;
if ( this.lz4wasmInstance !== null ) { return this; }
return null;
});
this.lz4wasmInstance.catch(( ) => {
this.lz4wasmInstance = null;
return null;
});
}
return this.lz4wasmInstance;
},
reset: function() {
this.lz4wasmInstance = undefined;
},
bytesInUse: function() {
return this.lz4wasmInstance instanceof WebAssembly.Instance ?
this.lz4wasmInstance.exports.memory.buffer.byteLength :
0;
},
encodeBlock: function(input, outputOffset) {
if ( this.lz4wasmInstance instanceof WebAssembly.Instance === false ) {
throw new Error('LZ4BlockWASM: not initialized');
}
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return encodeBlock(this.lz4wasmInstance, input, outputOffset);
},
decodeBlock: function(input, inputOffset, outputSize) {
if ( this.lz4wasmInstance instanceof WebAssembly.Instance === false ) {
throw new Error('LZ4BlockWASM: not initialized');
}
if ( input instanceof ArrayBuffer ) {
input = new Uint8Array(input);
} else if ( input instanceof Uint8Array === false ) {
throw new TypeError();
}
return decodeBlock(this.lz4wasmInstance, input, inputOffset, outputSize);
}
};
/******************************************************************************/
})(this || self); // <<<< End of private namespace
/******************************************************************************/
Binary file not shown.
+169
View File
@@ -0,0 +1,169 @@
/*******************************************************************************
lz4-block-codec-any.js
A wrapper to instanciate a wasm- and/or js-based LZ4 block
encoder/decoder.
Copyright (C) 2018 Raymond Hill
BSD-2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Home: https://github.com/gorhill/lz4-wasm
I used the same license as the one picked by creator of LZ4 out of respect
for his creation, see https://lz4.github.io/lz4/
*/
'use strict';
/******************************************************************************/
(function(context) { // >>>> Start of private namespace
/******************************************************************************/
let wd = (function() {
let url = document.currentScript.src;
let match = /[^\/]+$/.exec(url);
return match !== null ?
url.slice(0, match.index) :
'';
})();
let removeScript = function(script) {
if ( !script ) { return; }
if ( script.parentNode === null ) { return; }
script.parentNode.removeChild(script);
};
let createInstanceWASM = function() {
if ( context.LZ4BlockWASM instanceof Function ) {
let instance = new context.LZ4BlockWASM();
return instance.init().then(( ) => { return instance; });
}
if ( context.LZ4BlockWASM === null ) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.src = wd + 'lz4-block-codec-wasm.js';
script.addEventListener('load', ( ) => {
if ( context.LZ4BlockWASM instanceof Function === false ) {
context.LZ4BlockWASM = null;
context.LZ4BlockWASM = undefined;
resolve(null);
} else {
let instance = new context.LZ4BlockWASM();
instance.init()
.then(( ) => {
resolve(instance);
})
.catch(error => {
reject(error);
});
}
});
script.addEventListener('error', ( ) => {
context.LZ4BlockWASM = null;
resolve(null);
});
document.head.appendChild(script);
removeScript(script);
});
};
let createInstanceJS = function() {
if ( context.LZ4BlockJS instanceof Function ) {
let instance = new context.LZ4BlockJS();
return instance.init().then(( ) => { return instance; });
}
if ( context.LZ4BlockJS === null ) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.src = wd + 'lz4-block-codec-js.js';
script.addEventListener('load', ( ) => {
if ( context.LZ4BlockJS instanceof Function === false ) {
context.LZ4BlockJS = null;
resolve(null);
} else {
let instance = new context.LZ4BlockJS();
instance.init()
.then(( ) => {
resolve(instance);
})
.catch(error => {
reject(error);
});
}
});
script.addEventListener('error', ( ) => {
context.LZ4BlockJS = null;
resolve(null);
});
document.head.appendChild(script);
removeScript(script);
});
};
/******************************************************************************/
context.lz4BlockCodec = {
createInstance: function(flavor) {
let instantiator;
if ( flavor === 'wasm' ) {
instantiator = createInstanceWASM;
} else if ( flavor === 'js' ) {
instantiator = createInstanceJS;
} else {
instantiator = createInstanceWASM || createInstanceJS;
}
return (instantiator)()
.then(instance => {
if ( instance ) { return instance; }
if ( flavor === undefined ) {
return createInstanceJS();
}
return null;
})
.catch(( ) => {
if ( flavor === undefined ) {
return createInstanceJS();
}
return null;
});
},
reset: function() {
context.LZ4BlockWASM = undefined;
context.LZ4BlockJS = undefined;
}
};
/******************************************************************************/
})(this || self); // <<<< End of private namespace
File diff suppressed because one or more lines are too long
Binary file not shown.
+1728
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "Audiomass - Audio Editor",
"short_name": "Audiomass - Audio Editor",
"start_url": ".",
"scope": ".",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"type": "image/png",
"sizes": "144x144"
}
]
}
+378
View File
@@ -0,0 +1,378 @@
(function ( w, d, PKAE ) {
'use strict';
function PKMrk ( app ) {
var q = this, max = 11, raf = 0;
var cols = ['#9dff6a', '#5af2ff', '#f557d2', '#ffd15c', '#ff8c35', '#b9c6ff'];
var ed = mk (), mt = mk ();
function mk () { return {l:[], u:1, a:null, v:null, off:null, s:1}; }
function mtOn () { var m = app.multitrack; return !!(m && m.IsOn && m.IsOn ()); }
function cx ( n ) { return n === 'mt' ? mt : n === 'ed' ? ed : mtOn () ? mt : ed; }
function nm ( s ) { s = (s || '').replace (/[\r\n\t]/g, ' ').trim (); return s ? s.substr (0, max) : ''; }
function color ( s ) { return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test (s || '') ? s : 0; }
function redraw () { if (!raf) raf = w.requestAnimationFrame (function () { raf = 0; q.draw (); }); }
function emit ( c, id ) {
if (id) c.a = id;
redraw ();
}
function dur ( c ) {
if (c === mt) {
var m = app.multitrack;
return m && m.GetDuration ? m.GetDuration () || 0 : 0;
}
var ws = app.engine && app.engine.wavesurfer;
return ws && ws.getDuration ? ws.getDuration () || 0 : 0;
}
function can ( c ) {
var m = app.multitrack;
return c === mt ? !!(m && m.HasClips && m.HasClips ()) : dur (c) > 0;
}
function at ( c, t ) {
var durr = dur ( c );
t = isFinite (t) ? +t : 0;
return t < 0 ? 0 : durr > 0 && t > durr ? durr : t;
}
function now ( c ) {
if (c === mt) {
var m = app.multitrack;
return m && m.GetCursor ? m.GetCursor () || 0 : 0;
}
var ws = app.engine && app.engine.wavesurfer;
return ws && ws.getDuration ? (ws.ActiveMarker || 0) * (ws.getDuration () || 0) : 0;
}
function seek ( c, t ) {
var durr = dur ( c );
app.fireEvent ('RequestSeekTo', durr > 0 ? at (c, t) / durr : 0);
}
function playing ( c ) {
var m = app.multitrack, ws = app.engine && app.engine.wavesurfer;
return c === mt ? !!(m && m.IsPlaying && m.IsPlaying ()) : !!(ws && ws.isPlaying && ws.isPlaying ());
}
function sort ( c ) {
c.l.sort (function ( a, b ) { return a.time === b.time ? (a.id > b.id ? 1 : -1) : a.time - b.time; });
}
function ix ( c, id ) {
for (var i = 0; i < c.l.length; ++i)
if (c.l[i].id === id) return i;
return -1;
}
function ser ( c ) {
for (var a = [], i = 0, m; i < c.l.length; ++i) {
m = c.l[i];
a[i] = {id:m.id, time:m.time, name:m.name, color:m.color, loop:!!m.loop};
}
return a;
}
function hist ( c, prev, desc ) {
app.fireEvent ('StateRequestPush', {type:'mrk', ctx:c === mt ? 'mt' : 'ed', desc:desc, markers:prev});
}
function make ( c, o ) {
o = o || {};
var a = at (c, o.time), id, n;
id = o.id || ('m' + c.u++);
n = ((id || '').match (/^m(\d+)$/) || 0)[1] / 1;
if (n >= c.u) c.u = n + 1;
return {
id:id,
time:a,
name:nm (o.name) || nm ('Marker ' + id.substr (1)),
color:color (o.color) || cols[(c.u - 2) % cols.length],
loop:!!o.loop
};
}
function load ( c, a, h ) {
var old = ser ( c );
c.l = [];
c.u = 1;
for (var i = 0; a && i < a.length; ++i) c.l[i] = make (c, a[i]);
if (h !== false) hist (c, old, 'Load Markers');
sort ( c );
c.a = c.l[0] ? c.l[0].id : null;
emit ( c );
}
function clear ( c, h ) {
if (!c.l.length) return false;
if (h !== false) hist (c, ser (c), 'Clear Markers');
c.l = [];
c.u = 1;
c.a = null;
emit ( c );
return true;
}
function add ( c, o, h ) {
if (!can (c)) return false;
var old = ser ( c ), m = make (c, o);
if (h !== false) hist (c, old, 'Add Marker');
c.l[c.l.length] = m;
sort ( c );
emit (c, m.id);
return m;
}
function rem ( c, id, h ) {
var i = ix (c, id);
if (i < 0) return false;
if (h !== false) hist (c, ser (c), 'Delete Marker');
c.l.splice (i, 1);
c.a = c.l[0] ? c.l[0].id : null;
emit ( c );
return true;
}
function ren ( c, id, name, h ) {
var i = ix (c, id);
if (i < 0) return false;
name = nm ( name );
if (!name || c.l[i].name === name) return false;
if (h !== false) hist (c, ser (c), 'Rename Marker');
c.l[i].name = name;
emit (c, id);
return true;
}
function jump ( c, dir, sel ) {
if (!c.l.length) return ;
for (var t = now (c), m = null, i = dir < 0 ? c.l.length - 1 : 0; dir < 0 ? i >= 0 : i < c.l.length; i += dir)
if (dir < 0 ? c.l[i].time < t - 0.001 : c.l[i].time > t + 0.001) { m = c.l[i]; break; }
if (!m) m = dir < 0 ? c.l[c.l.length - 1] : c.l[0];
if (sel) app.fireEvent ('RequestRegionSet', Math.min (t, m.time), Math.max (t, m.time));
else { emit (c, m.id); seek (c, m.time); }
}
function drop ( o ) {
var c = cx ();
o = o || {};
if (o.time !== undefined) return add (c, o);
add (c, {time:now (c), name:o.name, color:o.color});
}
function renameUi ( c, id ) {
var i = ix (c, id), mid = 'mrk_ren';
if (i < 0) return ;
new PKSimpleModal ({
title:'Rename Marker',
clss:'pk_fnt10',
ondestroy:function () {
app.ui.InteractionHandler.forceUnset (mid);
app.ui.KeyHandler.removeCallback (mid + 'esc');
app.ui.KeyHandler.removeCallback (mid + 'en');
},
buttons:[{title:'Save', clss:'pk_modal_a_accpt', callback:function ( m ) {
var v = nm (m.el_body.getElementsByTagName ('input')[0].value);
if (v) { ren (c, id, v); m.Destroy (); }
else OneUp ('Name is too short...', 1200);
}}],
body:'<label for="k_mrkr">Marker Name</label><input style="width:100%;box-sizing:border-box;min-width:0" maxlength="' + max + '" class="pk_txt" type="text" id="k_mrkr" />',
setup:function ( m ) {
app.ui.InteractionHandler.forceSet (mid);
app.ui.KeyHandler.addCallback (mid + 'esc', function () { if (app.ui.InteractionHandler.check (mid)) m.Destroy (); }, [27]);
app.ui.KeyHandler.addCallback (mid + 'en', function () { if (app.ui.InteractionHandler.check (mid)) m.els.bottom[0].click (); }, [13]);
setTimeout (function () {
if (!m.el) return ;
var inp = m.el.getElementsByTagName ('input')[0];
inp.value = c.l[i].name;
inp.focus ();
inp.selectionStart = inp.selectionEnd = inp.value.length;
}, 20);
}
}).Show ();
}
function view ( c, o ) {
var layer, nodes = {}, menu, rmenu, td, hs;
function host () { return o.h (); }
function stop ( e ) { e.preventDefault (); e.stopImmediatePropagation ? e.stopImmediatePropagation () : e.stopPropagation (); }
function hit ( e, m ) { return e.clientY - m.r.top <= 24; }
function node ( t ) {
while (t && t !== host ()) {
if (t.classList && t.classList.contains ('pk_mrkr')) return t;
t = t.parentNode;
}
}
function ensure ( p ) {
if (layer && layer.parentNode === p) return layer;
layer = d.createElement ('div');
layer.className = 'pk_mrkrl';
p.appendChild ( layer );
nodes = {};
return layer;
}
function mkNode ( id ) {
var n = d.createElement ('div'), b = d.createElement ('b');
n.className = 'pk_mrkr';
n.setAttribute ('data-id', id);
n.lbl = b;
n.appendChild ( b );
layer.appendChild ( n );
return nodes[id] = n;
}
function paint () {
var p = o.p (), mtr, h, stamp, i, m, n, cls, x, tr, id, lim;
if (!p) return ;
ensure ( p );
if (o.v && !o.v ()) {
if (layer._d !== 'none') { layer.style.display = 'none'; layer._d = 'none'; }
return ;
}
if (layer._d) { layer.style.display = ''; layer._d = ''; }
mtr = o.m ();
lim = p.scrollWidth || p.clientWidth || 0;
h = (Math.max (1, o.lh () - 24) >> 0) + 'px';
if (layer._h !== h) { layer.style.setProperty ('--m', h); layer._h = h; }
stamp = ++c.s;
for (i = 0; i < c.l.length; ++i) {
m = c.l[i]; n = nodes[m.id] || mkNode (m.id); x = mtr.x (m.time) >> 0;
if (lim && x >= lim) x = lim - 1;
tr = 'translate3d(' + x + 'px,0,0)';
cls = 'pk_mrkr' + (m.id === c.a ? ' pk_act' : '');
n._s = stamp;
if (n._c !== cls) { n.className = cls; n._c = cls; }
if (n._n !== m.name) { n.lbl.textContent = m.name; n._n = m.name; }
if (n._o !== m.color) { n.style.color = m.color; n._o = m.color; }
if (n._t !== tr) { n.style.transform = tr; n._t = tr; }
}
for (id in nodes)
if (nodes[id]._s !== stamp) { nodes[id].parentNode && nodes[id].parentNode.removeChild (nodes[id]); delete nodes[id]; }
}
function openMenu ( e, id ) {
if (!app._deps.ContextMenu) return false;
if (!menu) {
menu = new app._deps.ContextMenu (d.createElement ('div'));
menu.addOption ('Rename Marker', function () { renameUi (menu.c, menu.id); }, false);
menu.addOption ('Delete Marker', function () { rem (menu.c, menu.id); }, false);
menu.addOption ('Play From Here', function () {
var i = ix (menu.c, menu.id);
if (i >= 0) { seek (menu.c, menu.c.l[i].time); if (!playing (menu.c)) app.fireEvent ('RequestPlay'); }
}, false);
}
menu.c = c; menu.id = id; menu.open ( e );
return true;
}
function openRulerMenu ( e, t ) {
if (!app._deps.ContextMenu || !can (c)) return false;
if (!rmenu) {
rmenu = new app._deps.ContextMenu (d.createElement ('div'));
rmenu.addOption ('Add Marker Here', function () { add (rmenu.c, {time:rmenu.t}); }, false);
}
rmenu.c = c; rmenu.t = t; rmenu.open ( e );
return true;
}
function markerDown ( e, n ) {
var id = n.getAttribute ('data-id'), i = ix (c, id), m, start, old, sx, moved = false;
if (i < 0) return ;
stop ( e );
emit (c, id);
if (e.button === 2 || e.which === 3) return openMenu (e, id);
if (e.altKey) return renameUi (c, id);
if (app.ui && app.ui.InteractionHandler && !app.ui.InteractionHandler.checkAndSet ('marker')) return ;
m = c.l[i]; start = m.time; old = ser ( c ); sx = e.clientX;
function move ( ev ) {
var t = o.m ().t (ev.clientX);
if (Math.abs (ev.clientX - sx) > 2 || Math.abs (t - start) > 0.001) moved = true;
m.time = t; emit (c, id);
ev.preventDefault ();
}
function up ( ev ) {
d.removeEventListener ('mousemove', move);
d.removeEventListener ('mouseup', up);
if (app.ui && app.ui.InteractionHandler) app.ui.InteractionHandler.forceUnset ('marker');
if (moved) { sort ( c ); hist (c, old, 'Move Marker'); emit (c, id); }
else seek (c, start);
ev && ev.preventDefault ();
}
d.addEventListener ('mousemove', move, false);
d.addEventListener ('mouseup', up, false);
}
function down ( e ) {
var n = node (e.target), m;
if (n) return markerDown (e, n);
m = o.m ();
if (!hit (e, m)) return ;
if (e.button === 2 || e.which === 3) { stop ( e ); openRulerMenu (e, m.t (e.clientX)); return ; }
if ((e.button !== undefined && e.button !== 0) || (e.which && e.which !== 1)) return ;
td = {x:e.clientX, y:e.clientY};
}
function click ( e ) {
var n = node (e.target), m;
if (n) return stop ( e );
m = o.m ();
if (!hit (e, m)) return ;
stop ( e );
if (e.detail > 1) return add (c, {time:m.t (e.clientX)});
if (!td || Math.abs (e.clientX - td.x) + Math.abs (e.clientY - td.y) < 4) seek (c, m.t (e.clientX));
td = null;
}
if (c.off) c.off ();
hs = host ();
hs.addEventListener ('mousedown', down, true);
hs.addEventListener ('click', click, true);
c.off = function () {
hs.removeEventListener ('mousedown', down, true);
hs.removeEventListener ('click', click, true);
if (layer && layer.parentNode) layer.parentNode.removeChild (layer);
if (menu) menu.destroy ();
if (rmenu) rmenu.destroy ();
};
c.v = paint;
paint ();
}
q.edge = function ( dir ) {
var c = cx (), t = now ( c ), m = null, i;
if (!c.l.length) return false;
for (i = dir < 0 ? c.l.length - 1 : 0; dir < 0 ? i >= 0 : i < c.l.length; i += dir)
if (dir < 0 ? c.l[i].time < t - 0.004 : c.l[i].time > t + 0.004) { m = c.l[i]; break; }
if (!m) return false;
emit (c, m.id);
seek (c, m.time);
return true;
};
q.ser = function ( n ) { return ser (cx (n)); };
q.serEd = function () { return ser (ed); };
q.serMt = function () { return ser (mt); };
q.loadEd = function ( a, h ) { load (ed, a, h); };
q.loadMt = function ( a, h ) { load (mt, a, h); };
q.clearEd = function ( h ) { return clear (ed, h); };
q.wave = function ( ws, wave ) {
if (!ws || !wave) return ;
view (ed, {
h:function () { return wave; },
p:function () { return wave; },
lh:function () { return wave.clientHeight || 24; },
m:function () {
var r = wave.getBoundingClientRect (), durr = ws.getDuration ? ws.getDuration () || 0 : 0;
var vis = ws.VisibleDuration || durr || 1, left = ws.LeftProgress || 0, scale = r.width / Math.max (0.0001, vis);
return {r:r, x:function ( t ) { return (t - left) * scale; }, t:function ( x ) { return at (ed, left + (x - r.left) / scale); }};
}
});
};
q.mt = function ( main, ruler, px, vis ) {
if (!main || !ruler || !px) return ;
view (mt, {
h:function () { return ruler; },
p:function () { return ruler; },
v:vis,
lh:function () { return main.clientHeight || 24; },
m:function () {
var r = ruler.getBoundingClientRect (), scale = Math.max (1, px ());
return {r:r, x:function ( t ) { return t * scale; }, t:function ( x ) { return at (mt, (x - r.left) / scale); }};
}
});
};
function draw ( c ) { c.v && c.v (); }
q.drawEd = function () { draw ( ed ); };
q.drawMt = function () { draw ( mt ); };
q.draw = function () { draw (ed); draw (mt); };
app.listenFor ('MrkrAdd', drop);
app.listenFor ('MrkrPrv', function ( sel ) { jump (cx (), -1, sel); });
app.listenFor ('MrkrNxt', function ( sel ) { jump (cx (), 1, sel); });
app.listenFor ('DidZoom', redraw);
app.listenFor ('DidCursorCenter', redraw);
app.listenFor ('DidUpdateLen', redraw);
app.listenFor ('RequestResize', redraw);
app.listenFor ('DidUnloadFile', function () { clear (ed, false); });
app.listenFor ('StateDidPop', function ( state, undo ) {
if (!state || state.type !== 'mrk') return ;
load (cx (state.ctx), state.markers, false);
OneUp ((undo ? 'Undo ' : 'Redo ') + state.desc);
});
}
PKAE._deps.mrk = PKMrk;
})( window, document, PKAudioEditor );
+270
View File
@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>AudioMass - Multitrack Mixer</title>
<meta charset="utf-8" />
<style>
html,body{height:100%}
body{margin:0;background:#090d0e;color:#dfe7e8;overflow:hidden;font:12px Arial,sans-serif}
#m{box-sizing:border-box;height:100%;padding:26px 10px 10px;white-space:nowrap;overflow:auto;background:#090d0e;scrollbar-color:#1c2026 #0e1013}
#m::-webkit-scrollbar{width:10px;height:10px}
#m::-webkit-scrollbar-track{background:#0e1013}
#m::-webkit-scrollbar-thumb{background:#1c2026;border:2px solid #0e1013;border-radius:4px}
#m::-webkit-scrollbar-thumb:hover,#m::-webkit-scrollbar-thumb:active{background:#6a7380}
.s{display:inline-block;position:relative;box-sizing:border-box;width:92px;height:268px;margin-right:8px;padding:8px 7px;vertical-align:top;background:linear-gradient(#182023,#101416);border:1px solid #263338;border-radius:5px;box-shadow:inset 0 1px rgba(255,255,255,.05),0 2px 8px #000}
.s.sel{border-color:#56dbe3;box-shadow:0 0 12px rgba(80,220,225,.35),inset 0 1px rgba(255,255,255,.05)}
.s.master{border-color:#4b493c;background:linear-gradient(#1f211d,#121310)}
strong{display:block;height:26px;overflow:hidden;color:#f3f6f6;font:bold 13px/13px Arial,sans-serif}
.meter{height:7px;margin:5px 0 8px;background:#050708;border:1px solid #222c30;box-shadow:inset 0 1px 3px #000;overflow:hidden}
.meter i{display:block;height:100%;transform:scaleX(0);transform-origin:0 0;background:linear-gradient(90deg,#56dbe3,#77d26d 62%,#d4bd54 84%,#d45b57)}
p{margin:0 0 8px}
button{width:22px;height:22px;margin-right:2px;background:#06090a;color:#dfe7e8;border:1px solid #263338;border-radius:3px;cursor:pointer;font:13px Arial}
button.a{color:#111;background:#56dbe3;border-color:#56dbe3}
label{display:block;color:#8b9aa0;font:9px/12px monospace;text-transform:uppercase}
input{cursor:pointer}
.pn{height:44px;margin:0 -7px;display:flex;align-items:center;justify-content:center;gap:4px}
.pn label{width:27px;text-align:right}.pn em{width:25px;text-align:left}
.k{position:relative;width:30px;height:30px;margin:0;border-radius:50%;background:linear-gradient(145deg,#080b0c,#151b1e);box-shadow:inset 0 0 0 1px #263338,0 2px 7px #000;cursor:move}
.k i{position:absolute;left:14px;top:5px;width:3px;height:11px;background:#66e8ef;border-radius:2px;box-shadow:0 0 8px #66e8ef;transform-origin:50% 10px}
.vol{position:absolute;left:50%;bottom:78px;width:86px;height:18px;margin-left:-43px;transform:rotate(-90deg);transform-origin:50% 50%;appearance:none;background:transparent}
.vol::-webkit-slider-runnable-track{height:6px;background:#050708;border:1px solid #263338;border-radius:9px}
.vol::-webkit-slider-thumb{appearance:none;width:18px;height:18px;margin-top:-7px;border-radius:50%;background:#d7d2c3;border:1px solid #0a0d0e;box-shadow:0 1px 5px #000}
.vol::-moz-range-track{height:6px;background:#050708;border:1px solid #263338;border-radius:9px}
.vol::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#d7d2c3;border:1px solid #0a0d0e}
em,output{display:block;color:#b3b6b1;font:10px/14px monospace;text-align:center}
output{position:absolute;left:0;right:0;bottom:20px}
.empty{position:absolute;left:0;right:0;top:50%;margin-top:-9px;color:#4d5250;text-align:center;font:18px Arial;letter-spacing:1px}
#e,#d,#f{position:absolute;top:7px;right:7px;z-index:9;height:20px;padding:0 6px;background:#eaeaea;color:#111;border-radius:4px;opacity:.9;cursor:pointer;text-align:center;font:9px/20px Arial,sans-serif;user-select:none}
#d{min-width:40px}
#e{right:68px;width:10px;display:none}
#f{left:0;right:0;top:0;width:100%;height:4px;padding:0;border-radius:0;background:#333;display:none;line-height:0}
.b #e,.b #f{display:block}.b #e,.b #d{top:12px}.c #f,#f:hover{background:#444}
.b #m{padding-bottom:8px}
.b .s{height:236px}
.b .vol{bottom:58px;width:72px;margin-left:-36px}
.b output{bottom:12px}
</style>
</head>
<body>
<div id="m"><div class="empty">MULTITRACK MIXER</div></div>
<a id="d" onclick="dock()">DOCK</a>
<a id="e" onclick="remove()">X</a>
<a id="f" onmousedown="return drag(event)"></a>
<script>
var d = document;
var w = window;
var iframe = location.href.indexOf('?iframe') === -1 ? 0 : 1;
var box = d.getElementById ('m');
var sig = '';
var els = {};
var last = 0;
if (iframe) {
d.body.className = 'b';
d.getElementById ('d').textContent = 'UNDOCK';
}
function host () {
return iframe ? w.parent : w.opener;
}
function mt () {
var h = host ();
return h && h.PKAudioEditor && h.PKAudioEditor.multitrack;
}
function call ( id, key, val, done ) {
var m = mt ();
return m && m.MixerSet && m.MixerSet ( id, key, val, done );
}
w.remove = function () {
var h = host ();
h && h.PKAudioEditor && h.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'mix', [1, 1]);
};
w.drag = function ( e ) {
e.preventDefault ();
e.stopPropagation ();
w.parent.PKAudioEditor.ui.Dock ('RequestDragI', 'mix', [e.screenX, e.screenY]);
return false;
};
w.dock = function () {
var h = host ();
if (!h || !h.PKAudioEditor) return ;
if (!iframe) {
h.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'mix', [1, 1]);
w.close && w.close ();
return ;
}
var frm = w.parent.document.getElementById ('pk_frmix');
var t = 1;
if (frm && frm.getBoundingClientRect) {
var r = frm.getBoundingClientRect ();
t = [(w.parent.screenLeft + r.left + 100) || 0, (w.parent.screenTop + r.top + 25) || 0];
}
w.parent.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'mix', [t, 0]);
};
function make ( t ) {
var s = d.createElement ('section');
var master = t.id === 'master';
s.className = 's' + (master ? ' master' : '');
s.innerHTML = master ?
'<strong></strong><div class="meter"><i></i></div><label>level</label><input class="vol" type="range" min="0" max="1" step=".01"><output></output>' :
'<strong></strong><div class="meter"><i></i></div><p><button>M</button><button>S</button><button>R</button></p><div class="pn"><label>pan</label><div class="k"><i></i></div><em></em></div><input class="vol" type="range" min="0" max="1" step=".01"><output></output>';
s.getElementsByTagName ('strong')[0].textContent = t.name;
var e = els[t.id] = {
el:s,
meter:s.getElementsByTagName ('i')[0],
vol:s.getElementsByClassName ('vol')[0],
out:s.getElementsByTagName ('output')[0]
};
if (!master) {
var b = s.getElementsByTagName ('button');
e.mute = b[0]; e.solo = b[1]; e.rec = b[2];
e.pan = s.getElementsByClassName ('k')[0];
e.panv = s.getElementsByTagName ('em')[0];
e.mute.onclick = function ( ev ) { ev.stopPropagation (); call ( t.id, 'mute', !e.mute.classList.contains ('a'), 1 ); };
e.solo.onclick = function ( ev ) { ev.stopPropagation (); call ( t.id, 'solo', !e.solo.classList.contains ('a'), 1 ); };
e.rec.onclick = function ( ev ) { ev.stopPropagation (); call ( t.id, 'rec', !e.rec.classList.contains ('a'), 1 ); };
bindPan ( e.pan, t.id );
s.onclick = function ( ev ) {
if (/BUTTON|INPUT/.test (ev.target.tagName) ||
(ev.target.classList && ev.target.classList.contains ('k')) ||
(ev.target.parentNode.classList && ev.target.parentNode.classList.contains ('k'))) return ;
call ( t.id, 'select', 1, 1 );
};
}
e.vol.oninput = function () { call ( t.id, 'vol', this.value, 0 ); };
e.vol.onchange = function () { call ( t.id, 'vol', this.value, 1 ); };
e.vol.ondblclick = function () { this.value = 1; call ( t.id, 'vol', 1, 1 ); };
return s;
}
function panKnob ( el, v ) {
el._v = v;
el.getElementsByTagName ('i')[0].style.transform = 'rotate(' + (v * 65) + 'deg)';
}
function bindPan ( el, id ) {
el.onpointerdown = function ( ev ) {
ev.preventDefault ();
ev.stopPropagation ();
var x = ev.clientX, y = ev.clientY, v = el._v || 0;
function move ( e ) {
var n = Math.max (-1, Math.min (1, v + (e.clientX - x - e.clientY + y) / 80));
panKnob ( el, n );
call ( id, 'pan', n, 0 );
}
function up () {
d.removeEventListener ('pointermove', move);
call ( id, 'pan', el._v || 0, 1 );
}
d.addEventListener ('pointermove', move);
d.addEventListener ('pointerup', up, {once:true});
};
el.ondblclick = function () {
panKnob ( el, 0 );
call ( id, 'pan', 0, 1 );
};
}
function build ( list ) {
box.innerHTML = '';
els = {};
for (var i = 0; i < list.length; ++i) box.appendChild ( make ( list[i] ) );
}
function panText ( v ) {
return v < -0.01 ? 'L' + ((-v * 100) >> 0) : v > 0.01 ? 'R' + ((v * 100) >> 0) : 'C';
}
function cls ( el, on ) {
if (el && el._on !== !!on) {
el._on = !!on;
el.className = on ? 'a' : '';
}
}
function paint ( t ) {
var e = els[t.id];
if (!e) return ;
if (e.sel !== t.sel) {
e.sel = t.sel;
e.el.classList[t.sel ? 'add' : 'remove'] ('sel');
}
if (e.meter_v !== t.meter) {
e.meter_v = t.meter;
e.meter.style.transform = 'scaleX(' + (t.meter || 0) + ')';
}
if (e.vol_v !== t.vol) {
e.vol_v = t.vol;
e.vol.value = t.vol;
e.out.textContent = ((t.vol * 100) >> 0) + '%';
}
if (t.id !== 'master') {
if (e.pan_v !== t.pan) {
e.pan_v = t.pan;
panKnob ( e.pan, t.pan );
e.panv.textContent = panText ( t.pan );
}
cls ( e.mute, t.mute );
cls ( e.solo, t.solo );
cls ( e.rec, t.rec );
}
}
function update () {
var m = mt ();
var data = m && m.MixerData && m.MixerData ();
if (!data || !data.on) {
if (sig) {
sig = '';
box.innerHTML = '<div class="empty">OPEN MULTITRACK</div>';
}
return ;
}
var list = data.tracks.concat ([{
id:'master',
name:'Master',
vol:data.master.vol,
meter:data.master.meter
}]);
var next = list.map (function ( t ) { return t.id + ':' + t.name; }).join ('|');
if (next !== sig) {
sig = next;
build ( list );
}
for (var i = 0; i < list.length; ++i) paint ( list[i] );
}
function loop ( t ) {
if (t - last > 50) {
last = t;
update ();
}
w.requestAnimationFrame ( loop );
}
var last_press = 0;
d.addEventListener ('keydown', function ( e ) {
if ((e.keyCode || e.which) !== 32) return ;
e.preventDefault ();
e.stopPropagation ();
if (e.timeStamp - last_press < 100) return ;
last_press = e.timeStamp;
var h = host ();
h && h.PKAudioEditor && h.PKAudioEditor.ui.Dock ('RequestKeyDown', 32);
}, true);
w.onunload = function () {
w.destroy && w.destroy ( iframe );
w.destroy = null;
};
w.requestAnimationFrame ( loop );
</script>
</body>
</html>
+418
View File
@@ -0,0 +1,418 @@
(function ( w, d ) {
var _id = 0;
function PKSimpleModal ( config ) {
var q = this;
this.id = config.id ? config.id : (++_id);
var el = d.createElement ('div');
this.els = {
toolbar:[],
bottom:[]
};
el.className = 'pk_modal ' + (config.clss ? config.clss : '');
q.el = el;
// backdrop
var el_back = d.createElement ('div');
el_back.className = 'pk_modal_back';
this.el_back = el_back;
// var centerer
var el_cont = d.createElement ('div');
el_cont.className = 'pk_modal_cnt';
this.el_cont = el_cont;
// title
var el_title = d.createElement ('div');
el_title.className = 'pk_noselect pk_modal_title';
el_title.innerHTML = '<span>'+ (config.title || '') +'</span>';
el.appendChild ( el_title );
this.el_title = el_title;
// main
var el_main = d.createElement ('div');
el_main.className = 'pk_modal_main';
el.appendChild ( el_main );
this.el_body = el_main;
// bottom buttons
var el_bottom = d.createElement ('div');
el_bottom.className = 'pk_noselect pk_modal_bottom';
// -----------
var a_cancel = d.createElement ('a');
a_cancel.innerHTML = 'CANCEL';
a_cancel.className = 'pk_modal_cancel pk_modal_a_bottom';
a_cancel.onclick = function () {
q.Destroy ();
};
el_bottom.appendChild ( a_cancel );
// check if we need to construct more buttons from the config...
if (config.buttons && config.buttons.length > 0)
{
for (var i = 0; i < config.buttons.length; ++i)
{
var curr = config.buttons[i];
if (!curr.title || !curr.callback) continue;
var a_bottom = d.createElement ('a');
a_bottom.innerHTML = curr.title;
a_bottom.className = 'pk_modal_a_bottom ' + (curr.clss ? curr.clss : '');
if (curr.callback)
{
(function ( callback ) {
a_bottom.onclick = function () {
callback ( q );
};
})( curr.callback );
}
q.els.bottom.push (a_bottom);
el_bottom.appendChild ( a_bottom );
}
}
el.appendChild ( el_bottom );
// -----
if (config.toolbar && config.toolbar.length > 0)
{
for (var i = 0; i < config.toolbar.length; ++i)
{
var curr = config.toolbar[i];
if (!curr.title || !curr.callback) continue;
var a_link = d.createElement ('a');
a_link.innerHTML = curr.title + (curr.tooltip ? '<span>' + curr.tooltip + '</span>' : '');
a_link.className = 'pk_modal_a_top ' + (curr.clss ? curr.clss : '');
el_title.appendChild ( a_link );
if (curr.callback)
{
(function ( callback ) {
a_link.onclick = function () {
callback ( q, this );
};
})( curr.callback );
}
q.els.toolbar.push (a_link);
}
}
this.ondestroy = config.ondestroy;
if (config.body) q.el_body.innerHTML = config.body;
if (config.onpreset) this.onpreset = config.onpreset;
if (config.setup) config.setup ( this );
};
PKSimpleModal.prototype.Show = function () {
this.el_back.appendChild ( this.el_cont );
this.el_cont.appendChild ( this.el );
d.body.appendChild ( this.el_back );
return (this);
};
PKSimpleModal.prototype.Destroy = function () {
if (this.ondestroy) {
this.ondestroy ( this );
this.ondestroy = null;
}
this.els = null;
d.body.removeChild ( this.el_back );
};
// Extended modal
function PKAudioFXModal ( config, app ) {
var toolbar = null;
if (config.preview)
{
toolbar = [
{
title:'ON',
clss:'pk_inact',
tooltip:'Toggle Bypass',
callback: function ( q, el ) {
app.fireEvent ('RequestActionFX_TOGGLE');
}
},
{
title:'Preview',
callback: function ( q ) {
config.preview && config.preview ( q );
}
}
];
}
var inner_modal = new PKSimpleModal({
id: config.id,
title: config.title,
clss: config.clss,
presets: config.presets,
updateFilter: config.updateFilter,
ondestroy: function ( q ) {
app.fireEvent ('DidCloseFX_UI');
app.stopListeningFor ('DidStartPreview', q._evstart);
app.stopListeningFor ('DidStopPreview', q._evstop);
app.stopListeningFor ('DidTogglePreview', q._evtoggle);
app.stopListeningFor ('DidSetPresets', q._updatePresets);
app.stopListeningFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
app.stopListeningFor ('RequestSetPresetActive', q._updpreset);
if (q._upd_t) {
w.clearTimeout ( q._upd_t );
q._upd_t = 0;
}
app.fireEvent ('RequestActionFX_PREVIEW_STOP');
// if preview remove callback
app.ui.KeyHandler.removeCallback ('ksp' + q.id);
config.ondestroy && config.ondestroy ( q );
},
toolbar: toolbar,
buttons: config.buttons,
body: config.body,
onpreset: config.onpreset,
setup:function( q ) {
app.fireEvent ('RequestActionFX_TOGGLE', 1);
var slf = this;
app.ui.KeyHandler.addCallback ('ksp' + q.id, function ( key, map ) {
if (!app.ui.InteractionHandler.check ('modalfx')) return ;
var tb = slf.toolbar;
if (tb && tb.length > 0)
{
var k = tb.length;
while (k-- > 0) {
if (tb[k].title === 'Preview') {
tb[k].callback ( q );
break;
}
}
}
}, [32]);
q._evstart = function () {
q.els.toolbar[0].classList.remove ('pk_inact');
q.els.toolbar[1].classList.add ('pk_act');
};
q._evstop = function () {
q.els.toolbar[0].classList.add ('pk_inact');
q.els.toolbar[1].classList.remove ('pk_act');
}
q._evtoggle = function ( val ) {
var el = q.els.toolbar[0];
el.firstChild.nodeValue = val ? 'ON' : 'OFF';
};
q._updpreview = function ( val ) {
var sel_opt = q.el_presets.options[q.el_presets.selectedIndex];
var btn = q.el.getElementsByClassName('pk_sel_edt')[0];
if (val === 't')
{
if (q._upd_t) w.clearTimeout ( q._upd_t );
q._upd_t = 0;
if (sel_opt && sel_opt.getAttribute('data-custom')) {
btn.style.visibility = 'visible';
btn.style.opacity = '1';
app.stopListeningFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
}
else
{
btn.style.visibility = 'hidden';
btn.style.opacity = '0';
app.stopListeningFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
q._upd_t = w.setTimeout(function (){
q._upd_t = 0;
app.listenFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
}, 100);
}
return ;
}
btn.style.visibility = 'visible';
btn.style.opacity = '1';
app.stopListeningFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
};
q._updpreset = function ( fx_id, preset_id ) {
if (fx_id && fx_id !== q.id) {
return ;
}
var opts = q.el_presets.getElementsByTagName('option');
var ll = opts.length;
var curr = null;
while (ll-- > 0) {
curr = opts[ll];
if (curr.getAttribute('data-custom') === preset_id) {
curr.selected = 'selected';
break;
}
}
};
q._updatePresets = function ( fx_id, presets ) {
if (fx_id && fx_id !== q.id) {
return ;
}
var d = document;
var sel_presets = q.el.getElementsByClassName ('pk_sel');
// if presets exist remove them
if (sel_presets.length > 0)
{
sel_presets = sel_presets[0];
var opts = sel_presets.getElementsByTagName('option');
var ll = opts.length;
var curr = null;
while (ll-- > 0) {
curr = opts[ll];
if (curr.getAttribute('data-custom')) {
sel_presets.removeChild( curr );
}
}
if (presets.length === 0) return ;
var opt = d.createElement ('option');
// opt.value = '---custom----';
opt.setAttribute ('disabled', '1');
opt.setAttribute ('data-custom', '1');
opt.innerHTML = '----custom-----';
sel_presets.appendChild( opt );
for (var i = 0; i < presets.length; ++i)
{
var opt = d.createElement ('option');
var curr = presets[ i ];
opt.value = curr.val;
opt.setAttribute ('data-custom', curr.id);
opt.innerHTML = curr.name;
sel_presets.appendChild( opt );
}
return ;
}
else
{
sel_presets = d.createElement ('select');
sel_presets.className = 'pk_sel';
}
if (presets.length === 0) return ;
for (var i = -1; i < presets.length; ++i)
{
var opt = d.createElement ('option');
if (i === -1)
{
opt.value = 'null';
opt.innerHTML = 'Presets';
}
else
{
var curr = presets[ i ];
opt.value = curr.val;
opt.innerHTML = curr.name;
}
sel_presets.appendChild( opt );
}
sel_presets.onchange = function () {
var val_arr = this.value.split(',');
var els = q.el.getElementsByTagName('input');
q._updpreview ('t');
if (q.onpreset)
{
q.onpreset (this.value);
return ;
}
var len = els.length;
for (var i = 0; i < len; ++i) {
if (!val_arr[ i ]) break;
var curr_val = val_arr[ i ].trim ();
var curr_input = els[ i ];
if (curr_val === 'null') continue;
if (curr_input.type === 'checkbox' || curr_input.type === 'radio')
curr_input.checked = curr_val;
else
{
curr_input.value = curr_val;
curr_input.oninput && curr_input.oninput.apply (curr_input);
}
}
};
var btm = q.el.getElementsByClassName('pk_modal_bottom')[0];
btm.appendChild ( sel_presets );
q.el_presets = sel_presets;
// now add preset edit button
var edit_presets = d.createElement ('a');
edit_presets.className = 'pk_sel_edt';
edit_presets.innerHTML = '...<span>Save or Modify preset</span>';
edit_presets.onclick = function () {
app.fireEvent ('RequestSavePreset');
};
btm.appendChild ( edit_presets );
app.listenFor ('RequestActionFX_UPDATE_PREVIEW', q._updpreview);
app.listenFor ('RequestSetPresetActive', q._updpreset);
};
app.listenFor ('DidStartPreview', q._evstart);
app.listenFor ('DidStopPreview', q._evstop);
app.listenFor ('DidTogglePreview', q._evtoggle);
app.fireEvent ('DidOpenFX_UI', q);
if (config.updateFilter) q.updateFilter = config.updateFilter;
if (config.presets)
{
q._updatePresets (null, config.presets);
if (config.custom_pres) q._updatePresets (null, config.custom_pres);
app.listenFor ('DidSetPresets', q._updatePresets);
}
config.setup && config.setup ( q );
}
});
return (inner_modal);
};
w.PKSimpleModal = PKSimpleModal;
w.PKAudioFXModal = PKAudioFXModal;
})( window, document );
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
(function ( w, d ) {
function OneUp ( _text, _time, _clss ) {
var el = d.createElement ('div');
var cl = 'pk_oneup pk_noselect';
el.style.cssText = 'margin-top:20px;opacity:0';
if (_clss) cl = cl + ' ' + _clss;
el.className = cl;
el.innerHTML = _text || '';
d.body.appendChild ( el );
setTimeout (function() {
el.style.cssText = 'margin-top:0px;opacity:1';
setTimeout (function() {
el.style.cssText = 'margin-top:-20px;opacity:0';
setTimeout (function() {
el.parentNode.removeChild ( el );
el = null;
}, 330);
}, _time || 720);
}, 25);
}
w.OneUp = OneUp;
})( window, document );
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
class PKRec extends AudioWorkletProcessor {
constructor ( opts ) {
super ();
opts = opts || {};
this.b = new Float32Array (opts.processorOptions && opts.processorOptions.size || 4096);
this.i = 0;
this.done = false;
this.port.onmessage = this.flush.bind ( this );
}
flush () {
this.done = true;
if (this.i) {
var b = this.b.subarray (0, this.i).slice (0);
this.i = 0;
this.port.postMessage ( b, [b.buffer] );
}
this.port.postMessage (0);
}
process ( ins ) {
if (this.done) return false;
var ch = ins[0] && ins[0][0];
if (!ch) return true;
var b = this.b;
var i = this.i;
var l = b.length;
for (var off = 0; off < ch.length;) {
var n = Math.min (l - i, ch.length - off);
b.set (ch.subarray (off, off + n), i);
i += n;
off += n;
if (i === l) {
this.port.postMessage ( b, [b.buffer] );
b = this.b = new Float32Array ( l );
i = 0;
}
}
this.i = i;
return true;
}
}
registerProcessor ('pk-recorder', PKRec);
+373
View File
@@ -0,0 +1,373 @@
(function ( w, d, PKAE ) {
'use strict';
var PKREC = function ( app ) {
var q = this;
var media_stream_source = null;
var audio_stream = null;
var audio_context = null;
var script_processor = null;
var recorder_node = null;
var monitor_node = null;
var capture_opts = null;
var capture_id = 0;
var buffer_size = 2048 * 2;
var channel_num = 1;
var channel_num_out = 1;
var is_active = false;
var is_starting = false;
var is_stopping = false;
var starting_offset = 0;
var ending_offset = 0;
var sample_rate = 0;
var source_sample_rate = 0;
var temp_buffers = [];
var temp_buffer_index = -1;
var draw_samples = 0;
var skip_samples = 0;
var aggr = null;
var aggr_i = 0;
var end_record_func = null;
var start_record_func = null;
var curr_offset = 0;
var first_skip = 8;
function reportError ( error, cb ) {
is_starting = false;
stopCapture ();
if (cb) {
cb ( error );
return ;
}
app.fireEvent ('ErrorRec');
app.fireEvent ('ShowError', error && error.message ? error.message : 'No recording device found');
}
function flushAgg () {
if (!aggr || !aggr_i || !capture_opts || !capture_opts.ondata) return ;
capture_opts.ondata ( aggr.subarray (0, aggr_i).slice (0) );
aggr_i = 0;
}
function pushInput ( input, owned ) {
if (!capture_opts || !capture_opts.ondata || !input) return ;
var size = capture_opts.chunkSize || buffer_size;
if (input.length === size && !aggr_i) {
capture_opts.ondata ( owned ? input : input.slice (0) );
return ;
}
if (!aggr || aggr.length !== size) aggr = new Float32Array ( size );
for (var off = 0; off < input.length;) {
var n = Math.min (size - aggr_i, input.length - off);
aggr.set (input.subarray (off, off + n), aggr_i);
aggr_i += n;
off += n;
if (aggr_i === size) {
capture_opts.ondata ( aggr );
aggr = new Float32Array ( size );
aggr_i = 0;
}
}
}
function connectNode () {
monitor_node = audio_context.createGain ();
monitor_node.gain.value = 0;
media_stream_source.connect ( recorder_node );
recorder_node.connect ( monitor_node );
monitor_node.connect ( audio_context.destination );
}
function startScriptNode () {
script_processor = audio_context.createScriptProcessor (
buffer_size, channel_num, channel_num_out
);
recorder_node = script_processor;
script_processor.onaudioprocess = function ( ev ) {
pushInput ( ev.inputBuffer.getChannelData (0) );
};
connectNode ();
}
function startWorkletNode () {
recorder_node = new w.AudioWorkletNode (audio_context, 'pk-recorder', {
numberOfInputs: 1,
numberOfOutputs: 1,
outputChannelCount: [1],
processorOptions: {size: capture_opts.chunkSize || buffer_size}
});
recorder_node._pk_wk = true;
recorder_node.port.onmessage = function ( ev ) {
if (ev.data !== 0) pushInput ( ev.data, true );
};
connectNode ();
}
function loadWorklet () {
if (!audio_context.audioWorklet || !w.AudioWorkletNode)
return Promise.reject ();
if (!audio_context._pk_rec_wk)
audio_context._pk_rec_wk = audio_context.audioWorklet.addModule ('recorder-worklet.js');
return audio_context._pk_rec_wk;
}
this.startCapture = function ( opts ) {
if (is_active || is_starting || is_stopping) return false;
opts = opts || {};
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !opts.ctx) {
reportError ( null, opts.onerror );
return false;
}
audio_context = opts.ctx;
audio_context.resume && audio_context.resume ();
capture_opts = opts;
source_sample_rate = audio_context.sampleRate;
is_starting = true;
aggr = null;
aggr_i = 0;
var id = ++capture_id;
var audio_constraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
navigator.mediaDevices.getUserMedia ({
audio: audio_constraints,
video: false
}).then(function ( stream ) {
if (id !== capture_id) {
stream.getTracks ().forEach (function ( t ) { t.stop (); });
return ;
}
audio_stream = stream;
media_stream_source = audio_context.createMediaStreamSource ( stream );
return loadWorklet ().then ( startWorkletNode, startScriptNode );
}).then(function () {
if (id !== capture_id || !recorder_node) return ;
is_starting = false;
is_active = true;
capture_opts.onstart && capture_opts.onstart ();
}).catch(function ( error ) {
if (id === capture_id) reportError ( error, opts.onerror );
});
return true;
};
function finishCapture ( done ) {
flushAgg ();
if (script_processor) script_processor.onaudioprocess = null;
if (recorder_node && recorder_node.port) recorder_node.port.onmessage = null;
if (recorder_node) { try { recorder_node.disconnect (); } catch (e) {} }
if (monitor_node) { try { monitor_node.disconnect (); } catch (e2) {} }
if (media_stream_source) { try { media_stream_source.disconnect (); } catch (e3) {} }
if (audio_stream) {
audio_stream.getTracks ().forEach(function ( stream ) {
stream.stop ();
});
}
media_stream_source = null;
audio_stream = null;
script_processor = null;
recorder_node = null;
monitor_node = null;
capture_opts = null;
aggr = null;
aggr_i = 0;
is_stopping = false;
done && done ();
}
function stopCapture ( done ) {
if (is_stopping) return ;
++capture_id;
is_starting = false;
is_active = false;
is_stopping = true;
if (recorder_node && recorder_node._pk_wk && recorder_node.port) {
var did = false;
recorder_node.port.onmessage = function ( ev ) {
if (ev.data === 0) {
if (did) return ;
did = true;
finishCapture ( done );
}
else pushInput ( ev.data, true );
};
recorder_node.port.postMessage (0);
w.setTimeout(function () {
if (did) return ;
did = true;
finishCapture ( done );
}, 60);
return ;
}
finishCapture ( done );
}
this.stopCapture = stopCapture;
function fetchBufferFunction ( float_array ) {
if (skip_samples > 0) {
skip_samples -= float_array.length;
return ;
}
curr_offset += float_array.length / source_sample_rate * sample_rate;
if (ending_offset <= curr_offset) {
ending_offset > 0 && q.stop ();
return ;
}
temp_buffers[ ++temp_buffer_index ] = float_array;
draw_samples += float_array.length;
if (temp_buffer_index === 0 || draw_samples >= buffer_size * 4) {
requestAnimationFrame(function () {
draw_samples = 0;
app.engine.wavesurfer.DrawTemp ( starting_offset, temp_buffers );
});
}
}
this.isActive = function () {
return (is_active || is_starting);
};
this.setEndingOffset = function ( ending_offset_seconds ) {
ending_offset = ending_offset_seconds;
};
this.start = function ( _at_offset, _end_callback, _start_callback, _sample_rate ) {
if (is_active || is_starting) return (false);
starting_offset = _at_offset / 1;
if (isNaN (starting_offset) || !starting_offset) starting_offset = 0;
curr_offset = starting_offset;
audio_context = app.engine.wavesurfer.backend.getAudioContext ();
if (!audio_context) {
app.fireEvent ('ErrorRec');
app.fireEvent ('ShowError', 'No recording device found');
return (false);
}
if (audio_context.currentTime === 0) {
app.engine.wavesurfer.backend.source.start (0);
app.engine.wavesurfer.backend.source.stop (0);
app.engine.wavesurfer.backend.createSource ();
}
sample_rate = _sample_rate || (
app.engine.wavesurfer.backend.buffer ?
app.engine.wavesurfer.backend.buffer.sampleRate :
audio_context.sampleRate
);
source_sample_rate = audio_context.sampleRate;
skip_samples = first_skip * buffer_size;
draw_samples = 0;
end_record_func = function (offset, buffers, _callback) {
async function downsampleAudioBuffer ( buffers, sourceSampleRate, targetSampleRate ) {
var totalLength = buffers.reduce(function (sum, buf) {
return sum + buf.length;
}, 0);
var concatenated = new Float32Array ( totalLength );
var off = 0;
for (var i = 0; i < buffers.length; ++i) {
concatenated.set (buffers[i], off);
off += buffers[i].length;
}
var OfflineCtx = w.OfflineAudioContext || w.webkitOfflineAudioContext;
var audioBuffer = new OfflineCtx (1, totalLength, sourceSampleRate)
.createBuffer (1, totalLength, sourceSampleRate);
audioBuffer.copyToChannel (concatenated, 0, 0);
var duration = audioBuffer.duration;
var newLength = Math.ceil (duration * targetSampleRate);
var offlineCtx = new OfflineCtx (1, newLength, targetSampleRate);
var source = offlineCtx.createBufferSource ();
source.buffer = audioBuffer;
source.connect (offlineCtx.destination);
source.start (0);
var renderedBuffer = await offlineCtx.startRendering ();
return renderedBuffer.getChannelData (0);
}
if (source_sample_rate === sample_rate) {
_callback ();
_end_callback (offset, buffers);
return ;
}
downsampleAudioBuffer (buffers, source_sample_rate, sample_rate).then(function ( newBuffer ) {
_callback ();
_end_callback (offset, [newBuffer]);
}).catch(function () {
_callback ();
app.fireEvent ('ShowError', 'Could not resample recording');
});
};
start_record_func = _start_callback;
return q.startCapture ({
ctx: audio_context,
chunkSize: buffer_size,
ondata: fetchBufferFunction,
onstart: function () {
start_record_func && start_record_func ();
},
onerror: function ( error ) {
app.fireEvent ('ErrorRec');
if (error && error.message) app.fireEvent ('ShowError', error.message);
else app.fireEvent ('ShowError', 'No recording device found');
}
});
};
this.stop = function ( cancel_recording ) {
if (!is_active && !is_starting) return ;
stopCapture (function () {
app.engine.wavesurfer.DrawTemp ( null );
if (temp_buffers.length > 0 && !cancel_recording)
end_record_func && end_record_func ( starting_offset / sample_rate, temp_buffers, function () {});
else
end_record_func && end_record_func ( null, null, function () {});
sample_rate = 0;
source_sample_rate = 0;
first_skip = 8;
draw_samples = 0;
skip_samples = 0;
temp_buffer_index = -1;
starting_offset = ending_offset = 0;
temp_buffers = [];
audio_context = null;
end_record_func = start_record_func = null;
});
};
};
PKAE._deps.rec = PKREC;
})( window, document, PKAudioEditor );
File diff suppressed because it is too large Load Diff
Binary file not shown.
+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>window.update = function(){};</script>
<title>AudioMass - Spectrum Analyzer</title>
<style>
html,body{height:100%}
body{padding:0;margin:0;background:#111;position:relative;z-index:2;overflow:hidden;}
div{position:absolute;top:50%;left:0;right:0;z-index:1;margin-top:-18px;text-align:center;user-select:none;
font-size:28px;color:#191919;pointer-events:none;opacity:.75;font-family:Arial}
#e, #d, #f{
cursor:pointer;
position: absolute;
top: 7px;
right: 7px;
display: block;
height: 20px;
z-index: 99999;
background: #eaeaea;
left:auto;
border-radius:4px;
color:#111;
font:9px/20px Arial,sans-serif;
width: auto;
padding: 0 6px;
text-align: center;
opacity:0.9;
user-select:none;
}
.b #e, .b #d{
top:12px;
}
#d{
min-width:40px;
}
.b #e, .b #f{
display:block;
}
#e{
right: 68px;
width: 10px;
line-height: 20px;
padding: 0px 6px;
text-align: center;
opacity: 0.9;
display:none;
}
#f{
left: 0;
right: 0;
top: 0;
padding: 0;
width: 100%;
line-height: 0;
border-radius: 0;
height: 4px;
background: #333;
display:none;
user-select:none;
-moz-user-select:none;
}
.c #f, #f:hover{
background:#3C3C3C;
}
</style>
<meta charset="utf-8" />
</head>
<body>
<canvas id="fr" width="600" height="188" style="width:100%;height:100%;display:block"></canvas>
<div>SPECTRUM ANALYSER</div>
<a id="d" onclick="dock()">DOCK</a>
<a id="e" onclick="remove()">X</a>
<a id="f" onmousedown="drag(event)"></a>
<script>
var d = document;
var w = window;
var iframe = location.href.indexOf('?iframe') === -1 ? 0 : 1;
var canvas = d.getElementById('fr');
var ctx = canvas.getContext('2d', {alpha:true, antialias:false});
w.remove = function (){};
if (iframe) {
d.body.className = 'b';
d.getElementById ('d').textContent = 'UNDOCK';
w.remove = function () {
w.parent.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'sp', [1, 1]);
};
};
w.drag = function ( e ) {
e.preventDefault ();
e.stopPropagation ();
w.parent.PKAudioEditor.ui.Dock ('RequestDragI', 'sp', [e.screenX, e.screenY]);
};
w.dock = function () {
if (!iframe)
{
if (!w.opener || !w.opener.PKAudioEditor) {
return ;
}
w.opener.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'sp', [1, 1]);
w.close && w.close ();
}
else
{
var frm = w.parent.document.getElementById ('pk_fr' + 'sp');
var t = 1;
if (frm && frm.getBoundingClientRect) {
var rect = frm.getBoundingClientRect ();
t = [(w.parent.screenLeft + rect.left + 100)||0, (w.parent.screenTop + rect.top + 25)||0];
}
w.parent.PKAudioEditor.ui.Dock ('RequestShowFreqAn', 'sp', [t, 0]);
}
};
w.getGrayColor = function(value) {
return 'rgb(V, V, V)'.replace(/V/g, 255 - value);
};
w.getFullColor = function(value) {
var colorPalette = {
0: [0,0,0],
10: [75, 0, 159],
20: [104,0,251],
30: [131,0,255],
40: [155,18,157],
50: [175, 37, 0],
60: [191, 59, 0],
70: [206, 88, 0],
80: [223, 132, 0],
90: [240, 188, 0],
100: [255, 252, 0]
};
//floor to nearest 10:
var decimalised = 100 * value / 255
var percent = decimalised / 100;
var floored = 10* Math.floor(decimalised / 10);
var distFromFloor = decimalised - floored;
var distFromFloorPercentage = distFromFloor/10;
var rangeToNextColor;
if (decimalised < 100){
rangeToNextColor = [
colorPalette[floored + 10][0] - colorPalette[floored + 10][0],
colorPalette[floored + 10][1] - colorPalette[floored + 10][1],
colorPalette[floored + 10][2] - colorPalette[floored + 10][2]
];
} else {
rangeToNextColor = [0,0,0];
}
var color = [
colorPalette[floored][0] + distFromFloorPercentage * rangeToNextColor[0],
colorPalette[floored][1] + distFromFloorPercentage * rangeToNextColor[1],
colorPalette[floored][2] + distFromFloorPercentage * rangeToNextColor[2]
];
return "rgb(" + color[0] +", "+color[1] +"," + color[2]+")";
};
setTimeout(function () {
if (!iframe)
{
if (!w.opener || !w.opener.PKAudioEditor) return ;
}
var WIDTH = w.innerWidth, HEIGHT = w.innerHeight;
if (canvas.width != WIDTH)
{
canvas.width = WIDTH;
canvas.height = HEIGHT;
}
var bufferLength = 240;
var value_changed = false;
var speed = 3;
var tempCanvas = document.createElement('canvas');
tempCanvas.width = WIDTH;
tempCanvas.height = HEIGHT;
// console.log(this.canvas.height, this.tempCanvas.height);
var tempCtx = tempCanvas.getContext ('2d');
ctx.clearRect (0, 0, WIDTH, HEIGHT);
function draw( data ) {
var WIDTH = window.innerWidth, HEIGHT = window.innerHeight;
if (canvas.width != WIDTH || canvas.height != HEIGHT)
{
canvas.width = WIDTH;
canvas.height = HEIGHT;
tempCanvas.width = WIDTH;
tempCanvas.height = HEIGHT;
}
tempCtx.drawImage (canvas, 0, 0, WIDTH, HEIGHT);
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Iterate over the frequencies.
for (var i = 0; i < data.length; ++i)
{
var value;
// Draw each pixel with the specific color.
// if (this.log) {
// logIndex = this.logScale(i, data.length);
// value = data[logIndex];
// } else {
value = data[i];
// }
ctx.fillStyle = window.getFullColor(value);
var percent = i / data.length;
var y = Math.round (percent * HEIGHT);
// draw the line at the right side of the canvas
ctx.fillRect(WIDTH - speed, HEIGHT - y,
speed, speed);
}
// Translate the canvas.
ctx.translate(-speed, 0);
// Draw the copied image.
// console.log(this.width, this.height);
ctx.drawImage (tempCanvas, 0, 0, WIDTH, HEIGHT,
0, 0, WIDTH, HEIGHT);
// Reset the transformation matrix.
ctx.setTransform (1, 0, 0, 1, 0, 0);
value_changed = false;
};
w.draw = draw;
w.onunload = function () {
w.destroy && w.destroy ( iframe );
w.destroy = null;
};
w.update = function (freq_arr) {
if (!freq_arr)
ctx.clearRect (0, 0, WIDTH, HEIGHT);
else {
if (value_changed) return ;
value_changed = true;
window.requestAnimationFrame(function () {
draw (freq_arr);
});
}
};
var last_press = 0;
document.addEventListener ('keypress', function ( e ) {
if (e.keyCode !== 32) return ;
e.preventDefault ();
e.stopPropagation ();
if (e.timeStamp - last_press < 100) {
return ;
}
last_press = e.timeStamp;
if (!iframe) {
w.opener && w.opener.PKAudioEditor.ui.Dock ('RequestKeyDown', 32);
}
else {
w.parent && w.parent.PKAudioEditor.ui.Dock ('RequestKeyDown', 32);
}
});
}, 60);
</script>
</body>
</html>
+136
View File
@@ -0,0 +1,136 @@
(function ( PKAE ) {
'use strict';
function PKState ( _depth, app ) {
if (!_depth) _depth = 1;
var q = this;
var _id = 1;
var _fireEvent = app.fireEvent;
var _listenFor = app.listenFor;
var undo_state_list = [];
var redo_state_list = [];
function currentStateFor ( state ) {
var current = {
data: app.engine.wavesurfer.backend.buffer
};
if (state.type === 'mult' && app.multitrack)
current.mt = app.multitrack.getState ();
if (state.type === 'mrk' && app.mrk)
current.markers = app.mrk.ser (state.ctx);
return current;
}
function updateStateData ( state, current ) {
state.data = current.data;
if (current.mt) state.mt = current.mt;
if (current.markers) state.markers = current.markers;
}
q.getLastUndoState = function () {
return (undo_state_list [ undo_state_list.length - 1]);
};
q.pushUndoState = function ( state ) {
if (!state) return (false);
if (!state.id) state.id = ++_id;
if (undo_state_list.length >= _depth) undo_state_list.shift ();
if (undo_state_list.length > 0)
{
if (undo_state_list[undo_state_list.length - 1].id !== state.id - 1)
undo_state_list = [];
}
if (redo_state_list.length > 0)
{
if (redo_state_list[0].id !== state.id + 1)
redo_state_list = [];
}
undo_state_list.push ( state );
_fireEvent ( 'StatePush', undo_state_list.length );
_fireEvent ( 'DidStateChange', undo_state_list, redo_state_list);
return (true);
};
q.popUndoState = function () {
var last_state =undo_state_list.pop ();
if (last_state) {
if (redo_state_list.length > 0)
{
if (redo_state_list[0].id !== last_state.id + 1)
redo_state_list = [];
}
var current = currentStateFor ( last_state );
_fireEvent ( 'StateDidPop', last_state, 1 );
updateStateData ( last_state, current );
redo_state_list.unshift (last_state);
_fireEvent ( 'DidStateChange', undo_state_list, redo_state_list);
}
return (last_state);
};
q.shiftRedoState = function () {
var last_state = redo_state_list.shift ();
if (last_state) {
if (undo_state_list.length > 0)
{
if (undo_state_list[undo_state_list.length - 1].id !== last_state.id - 1)
undo_state_list = [];
}
var current = currentStateFor ( last_state );
_fireEvent ( 'StateDidPop', last_state, 0 );
updateStateData ( last_state, current );
undo_state_list.push (last_state);
_fireEvent ( 'DidStateChange', undo_state_list, redo_state_list);
}
return (last_state);
};
q.clearAllState = function () {
undo_state_list = [];
redo_state_list = [];
_fireEvent ( 'StateClearAll' );
_fireEvent ( 'DidStateChange', [], []);
};
_listenFor ('StateRequestPush', function ( _state ) {
q.pushUndoState ( _state );
});
_listenFor ('StateRequestUndo', function () {
q.popUndoState ();
});
_listenFor ('StateRequestRedo', function () {
q.shiftRedoState ();
});
_listenFor ('StateRequestClearAll', function () {
q.clearAllState ();
});
_listenFor ('StateRequestLastState', function () {
_fireEvent ('StateDidLastState', q.getLastUndoState ());
});
// -
};
PKAE._deps.state = PKState;
})( PKAudioEditor );
+65
View File
@@ -0,0 +1,65 @@
const CACHE_NAME = 'audiomass-production-v63';
const assets = [
'./',
'./manifest.json',
'./ico.png',
'./icon.png',
'./index.html',
'./all.css',
'./all.build.js',
'./recorder-worklet.js',
'./tempo-estimator.js',
'./tempo-worker.js',
'./wav.js',
'./lame.js',
'./flac.js',
'./libflac.js',
'./libflac.wasm',
'./lz4-block-codec-wasm.js',
'./lz4-block-codec.wasm',
'./rnn_denoise.js',
'./rnn_denoise.wasm',
'./fonts/icomoon.woff',
'./eq.html',
'./sp.html',
'./mix.html'//, './test.mp3'
];
self.addEventListener( 'install', function ( event ) {
event.waitUntil(( async function () {
const cache = await caches.open( CACHE_NAME );
await Promise.all( assets.map( function ( asset ) {
return cache.add( new Request ( asset, { cache: 'reload' } ) ).catch( function () {
console.warn( '[SW] Could not cache:', asset );
});
}));
})());
});
self.addEventListener( 'activate', function ( event ) {
event.waitUntil(( async function () {
const keys = await caches.keys();
await Promise.all( keys.map( function ( key ) {
if ( key !== CACHE_NAME ) return caches.delete( key );
}));
await self.clients.claim();
})());
});
self.addEventListener( 'fetch', async function ( event ) {
const request = event.request;
event.respondWith( cacheFirst( request ) );
});
self.addEventListener( 'message', function ( event ) {
if ( event.data === 'SKIP_WAITING' ) self.skipWaiting();
});
async function cacheFirst( request ) {
if ( request.method !== 'GET' ) return fetch( request );
const cachedResponse = await caches.match( request, { ignoreSearch: true } );
if ( cachedResponse === undefined ) return fetch( request );
return cachedResponse;
}
+212
View File
@@ -0,0 +1,212 @@
(function ( w ) {
'use strict';
function scoreLag ( flux, lag ) {
var score = 0;
var len = flux.length - lag;
if (len <= 0) return (0);
for (var i = lag; i < flux.length; ++i)
score += flux[i] * flux[i - lag];
return (score / len);
}
function makeFlux ( buffer ) {
var sr = buffer.sampleRate;
var hop = Math.max (256, (sr / 100) >> 0);
var frames = Math.max (1, buffer.length / hop >> 0);
var env = new Float32Array (frames);
var chans = buffer.numberOfChannels;
var ch0 = buffer.getChannelData (0);
var ch1 = chans > 1 ? buffer.getChannelData (1) : null;
for (var i = 0; i < frames; ++i) {
var start = i * hop;
var end = Math.min (buffer.length, start + hop);
var sum = 0;
var j = start;
if (ch1) {
for (; j < end; ++j)
sum += Math.abs (ch0[j]) + Math.abs (ch1[j]);
env[i] = sum / ((end - start) * 2);
}
else {
for (; j < end; ++j)
sum += Math.abs (ch0[j]);
env[i] = sum / (end - start);
}
}
var flux = new Float32Array (frames);
var mean = env[0] || 0;
flux[0] = mean;
for (i = 1; i < frames; ++i) {
var v = env[i] - env[i - 1];
if (v > 0) {
flux[i] = v;
mean += v;
}
}
mean = mean / Math.max (1, frames) * 1.25;
for (i = 0; i < frames; ++i)
flux[i] = Math.max (0, flux[i] - mean);
return ({ data: flux, rate: sr / hop });
}
function foldTempo ( bpm, min, max ) {
while (bpm < min) bpm *= 2;
while (bpm > max) bpm /= 2;
return (bpm);
}
function pickPeaks ( flux, rate ) {
var peaks = [];
var hold = Math.max (1, rate * 0.08 >> 0);
var last = -hold;
for (var i = 1; i < flux.length - 1; ++i) {
if (i - last < hold) continue;
if (flux[i] <= flux[i - 1] || flux[i] < flux[i + 1] || flux[i] <= 0)
continue;
peaks.push ({ pos: i, val: flux[i] });
last = i;
}
peaks.sort (function ( a, b ) { return b.val - a.val; });
if (peaks.length > 320) peaks.length = 320;
peaks.sort (function ( a, b ) { return a.pos - b.pos; });
return (peaks);
}
function intervalTempo ( flux, rate, min, max ) {
var peaks = pickPeaks (flux, rate);
var bins = {};
var best = 0;
var bestScore = 0;
var second = 0;
for (var i = 0; i < peaks.length; ++i) {
for (var j = i + 1; j < peaks.length && j < i + 16; ++j) {
var dist = (peaks[j].pos - peaks[i].pos) / rate;
if (dist <= 0) continue;
var bpm = foldTempo (60 / dist, min, max);
var key = Math.round (bpm);
var score = (peaks[i].val + peaks[j].val) / (j - i);
bins[key] = (bins[key] || 0) + score;
}
}
for (var k in bins) {
var val = bins[k];
if (val > bestScore) {
second = bestScore;
bestScore = val;
best = k / 1;
}
else if (val > second) {
second = val;
}
}
return ({
tempo: best,
score: bestScore,
confidence: bestScore ? (bestScore - second) / bestScore : 0
});
}
function analyze ( buffer, opts ) {
opts = opts || {};
if (!buffer || !buffer.length || buffer.duration < 2)
throw new Error ('Audio is too short to estimate tempo.');
var min = opts.minTempo || 60;
var max = opts.maxTempo || 200;
var env = makeFlux (buffer);
var flux = env.data;
var rate = env.rate;
var minLag = Math.max (1, Math.round (rate * 60 / max));
var maxLag = Math.min (flux.length - 1, Math.round (rate * 60 / min));
var bestLag = 0;
var bestScore = 0;
var secondScore = 0;
for (var lag = minLag; lag <= maxLag; ++lag) {
var score = scoreLag (flux, lag);
if (lag * 2 < flux.length) score += scoreLag (flux, lag * 2) * 0.35;
if (lag * 3 < flux.length) score += scoreLag (flux, lag * 3) * 0.20;
if (score > bestScore) {
secondScore = bestScore;
bestScore = score;
bestLag = lag;
}
else if (score > secondScore) {
secondScore = score;
}
}
if (!bestLag || !bestScore)
throw new Error ('Could not find a reliable tempo.');
var acTempo = 60 * rate / bestLag;
var intv = intervalTempo (flux, rate, min, max);
var tempo = acTempo;
if (intv.tempo) {
var ratio = Math.max (tempo, intv.tempo) / Math.min (tempo, intv.tempo);
if (ratio > 1.85 && ratio < 2.15)
tempo = intv.tempo;
else if (Math.abs (tempo - intv.tempo) < 8)
tempo = (tempo + intv.tempo) / 2;
else if (intv.confidence > 0.25)
tempo = intv.tempo;
bestLag = Math.max (1, Math.round (rate * 60 / tempo));
}
var phase = 0;
var phaseScore = 0;
for (var p = 0, phaseLen = Math.min (bestLag, flux.length); p < phaseLen; ++p) {
var ps = 0;
for (var k = p; k < flux.length; k += bestLag)
ps += flux[k];
if (ps > phaseScore) {
phaseScore = ps;
phase = p;
}
}
var period = bestLag / rate;
var offset = phase / rate;
var beats = Math.max (0, Math.floor ((buffer.duration - offset) / period));
var confidence = Math.max ((bestScore - secondScore) / bestScore, intv.confidence || 0);
confidence = Math.max (0, Math.min (100, confidence * 100));
return ({
tempo: Math.round (tempo * 10) / 10,
bpm: Math.round (tempo),
offset: Math.round (offset * 1000) / 1000,
beats: beats,
confidence: Math.round (confidence),
duration: Math.round (buffer.duration * 10) / 10
});
}
w.PKTempoEstimator = {
estimate: function ( buffer, opts ) {
return new Promise (function ( resolve, reject ) {
setTimeout (function () {
try { resolve (analyze (buffer, opts)); }
catch (e) { reject (e); }
}, 20);
});
}
};
})( typeof self !== 'undefined' ? self : window );
+34
View File
@@ -0,0 +1,34 @@
(function ( w ) {
'use strict';
importScripts ('tempo-estimator.js?v=mt2');
function makeBuffer ( data ) {
return ({
sampleRate: data.sampleRate,
length: data.length,
duration: data.length / data.sampleRate,
numberOfChannels: data.channels.length,
getChannelData: function ( index ) {
return data.channels[index] || data.channels[0];
}
});
}
w.onmessage = function ( e ) {
var msg = e.data || {};
if (msg.type !== 'estimate') return ;
w.PKTempoEstimator.estimate (makeBuffer (msg.buffer), msg.opts).then (
function ( ret ) {
w.postMessage ({ id: msg.id, result: ret });
},
function ( err ) {
w.postMessage ({
id: msg.id,
error: err && err.message ? err.message : 'Could not estimate tempo.'
});
}
);
};
})( self );
BIN
View File
Binary file not shown.
+3132
View File
File diff suppressed because it is too large Load Diff
+3703
View File
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
function interleave(L, R, Arr) {
var len = L.length + R.length;
var out = new Arr(len);
var i = 0, j = 0, n = L.length;
while (j < n) {
out[i++] = L[j];
out[i++] = R[j];
++j;
}
return out;
}
function writeString(view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function encodeWAV(samples, numChannels, sampleRate, bitDepth) {
var bytesPerSample = bitDepth / 8;
var dataLen = samples.length * bytesPerSample;
var isFloat = bitDepth === 32;
var fmtSize = isFloat ? 18 : 16;
var factSize = isFloat ? 12 : 0;
var totalSize = 12 + 8 + fmtSize + factSize + 8 + dataLen;
var buffer = new ArrayBuffer(totalSize);
var view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, totalSize - 8, true);
writeString(view, 8, 'WAVE');
var p = 12;
writeString(view, p, 'fmt ');
view.setUint32(p + 4, fmtSize, true);
view.setUint16(p + 8, isFloat ? 3 : 1, true);
view.setUint16(p + 10, numChannels, true);
view.setUint32(p + 12, sampleRate, true);
view.setUint32(p + 16, sampleRate * numChannels * bytesPerSample, true);
view.setUint16(p + 20, numChannels * bytesPerSample, true);
view.setUint16(p + 22, bitDepth, true);
if (isFloat) view.setUint16(p + 24, 0, true);
p += 8 + fmtSize;
if (isFloat) {
writeString(view, p, 'fact');
view.setUint32(p + 4, 4, true);
view.setUint32(p + 8, samples.length / numChannels, true);
p += 12;
}
writeString(view, p, 'data');
view.setUint32(p + 4, dataLen, true);
p += 8;
if (bitDepth === 16) {
for (var i = 0; i < samples.length; ++i, p += 2)
view.setInt16(p, samples[i], true);
} else if (bitDepth === 24) {
for (var i = 0; i < samples.length; ++i, p += 3) {
var v = samples[i];
view.setUint8(p, v & 0xff);
view.setUint8(p + 1, (v >> 8) & 0xff);
view.setUint8(p + 2, (v >> 16) & 0xff);
}
} else {
for (var i = 0; i < samples.length; ++i, p += 4)
view.setFloat32(p, samples[i], true);
}
return buffer;
}
var sample_rate = 44100;
var kbps = 128;
var channels = 1;
var bit_depth = 16;
var left_buf = null;
var right_buf = null;
var first_buffer = true;
onmessage = function( ev ) {
if (!ev.data) return ;
if (ev.data.sample_rate) {
sample_rate = ev.data.sample_rate / 1;
kbps = ev.data.kbps / 1;
channels = ev.data.channels / 1;
bit_depth = (ev.data.bit_depth / 1) || 16;
return ;
}
if (first_buffer) {
left_buf = ev.data;
first_buffer = false;
if (channels > 1) return ;
}
else if (channels > 1) {
right_buf = ev.data;
}
var Arr = bit_depth === 32 ? Float32Array :
bit_depth === 24 ? Int32Array :
Int16Array;
var L = new Arr (left_buf);
var R = right_buf ? new Arr (right_buf) : null;
var interleaved = R ? interleave (L, R, Arr) : L;
var encoded = encodeWAV(interleaved, channels, sample_rate, bit_depth);
var audioBlob = new Blob([encoded], { type: 'audio/wav' });
postMessage( audioBlob );
}
+93
View File
@@ -0,0 +1,93 @@
(function ( w, d, PKAE ) {
'use strict';
setTimeout(function () {
if (/(^|[?&])skipintro=1(&|$)/.test (w.location.search)) return ;
var scroll_hint = 0;
var showScrollHint = function () {
var tbc, el, r;
if (!PKAE.isMobile || scroll_hint) return ;
scroll_hint = 1;
tbc = PKAE.ui.el.getElementsByClassName ('pk_tbc')[0];
if (!tbc || tbc.scrollWidth <= tbc.clientWidth + 2) return ;
el = d.createElement ('i');
r = tbc.getBoundingClientRect ();
el.className = 'pk_tbhint';
el.innerHTML = '&#8250;';
el.style.top = ((r.top + r.height / 2 - 12) >> 0) + 'px';
PKAE.ui.el.appendChild ( el );
setTimeout (function () {
el.parentNode && el.parentNode.removeChild ( el );
}, 3000);
};
PKAudioEditor._deps.Wlc = function () {
var body_str = '';
var body_str2 = '';
var mobile_note = '';
if (PKAE.isMobile) {
mobile_note = '(Optimized for desktop - sorry)<br/><br/>';
body_str = 'Tips:<br/>Please make sure your device is not in silent mode. You might need to physically flip the silent switch. '+
'<img src="phone-switch.jpg" style="max-width:224px;max-height:126px;width:40%;margin: 10px auto; display: block;"/>'+
'<br/><br/>';
}
else {
body_str = 'Tips:<br/>Please keep in mind that most key shortcuts rely on the <strong>Shift + <u>key</u></strong> combo. (eg Shift+Z for undo, Shift+C copy, Shift+X cut... etc )<br/><br/>';
body_str2 = 'Check out the codebase on <a href="https://github.com/pkalogiros/audiomass" target="_blank">Github</a><br/><br/>'; // checkout the code on github
}
// Welcome to AudioMass,
var md = new PKSimpleModal({
title: '<font style="font-size:15px">Welcome to AudioMass</font>',
ondestroy: function( q ) {
PKAE.ui.InteractionHandler.on = false;
PKAE.ui.KeyHandler.removeCallback ('modalTemp');
showScrollHint ();
},
body:'<div style="overflow:auto;-webkit-overflow-scrolling:touch;max-width:580px;width:calc(100vw - 40px);max-height:calc(100vh - 340px);min-height:110px;font-size:13px; color:#95c6c6;padding-top:7px;">'+
mobile_note+
'AudioMass is a free, open source, web-based Audio and Waveform Editor.<br />It runs entirely in the browser with no backend and no plugins required!'+
'<br/><br/>'+
body_str+
'You can load any type of audio your browser supports and perform operations such as fade in, cut, trim, change the volume, '+
'and apply a plethora of audio effects.<br/><br/>'+
body_str2+
'I hope you enjoy the little music pieces. I wrote them a long time ago :)'+
'</div>',
setup:function( q ) {
PKAE.ui.InteractionHandler.checkAndSet ('modal');
PKAE.ui.KeyHandler.addCallback ('modalTemp', function ( e ) {
q.Destroy ();
}, [27]);
// ------
var scroll = q.el_body.getElementsByTagName('div')[0];
scroll.addEventListener ('touchstart', function(e){
e.stopPropagation ();
}, false);
scroll.addEventListener ('touchmove', function(e){
e.stopPropagation ();
}, false);
// ------
}
});
md.Show ();
document.getElementsByClassName('pk_modal_cancel')[0].innerHTML = '&nbsp; &nbsp; &nbsp; OK &nbsp; &nbsp; &nbsp;';
};
var change = 99;
var exists = w.localStorage && w.localStorage.getItem ('k');
if (!exists) {
change = 0;
w.localStorage && w.localStorage.setItem ('k', 1);
}
if ( ((Math.random () * 100) >> 0) < change) return ;
PKAudioEditor._deps.Wlc ();
}, 320);
})( window, document, PKAudioEditor );
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More