-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpitch-shift.js
More file actions
118 lines (110 loc) · 4.97 KB
/
Copy pathpitch-shift.js
File metadata and controls
118 lines (110 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import {
createChannelWriter, isChannelArray, mapInput, normalizeOptionsInput,
passThroughWriter, resolveRatio,
} from './host.js'
import formant from '@audio/shift-formant'
import ola from '@audio/shift-ola'
import vocoder from '@audio/shift-pvoc'
import phaseLock from '@audio/shift-pvoc-lock'
import transient from '@audio/shift-transient'
import psola from '@audio/shift-psola'
import wsola from '@audio/shift-wsola'
import granular from '@audio/shift-granular'
import paulstretch from '@audio/shift-paulstretch'
import sms from '@audio/shift-sms'
import hpss from '@audio/shift-hpss'
import sample from '@audio/shift-sample'
import hybrid from '@audio/shift-hybrid'
import delay from '@audio/shift-delay'
import lpc from '@audio/shift-lpc'
function selectMethod(opts) {
if (typeof opts?.method === 'function') {
return { fn: opts.method, name: opts.method.name || 'custom', reason: 'explicit-method' }
}
switch (opts?.method) {
case 'ola': return { fn: ola, name: 'ola', reason: 'explicit-method' }
case 'vocoder': return { fn: vocoder, name: 'vocoder', reason: 'explicit-method' }
case 'phase-lock':
case 'phaseLock': return { fn: phaseLock, name: 'phaseLock', reason: 'explicit-method' }
case 'transient': return { fn: transient, name: 'transient', reason: 'explicit-method' }
case 'formant': return { fn: formant, name: 'formant', reason: 'explicit-method' }
case 'psola': return { fn: psola, name: 'psola', reason: 'explicit-method' }
case 'wsola': return { fn: wsola, name: 'wsola', reason: 'explicit-method' }
case 'granular': return { fn: granular, name: 'granular', reason: 'explicit-method' }
case 'paulstretch': return { fn: paulstretch, name: 'paulstretch', reason: 'explicit-method' }
case 'sms': return { fn: sms, name: 'sms', reason: 'explicit-method' }
case 'hpss': return { fn: hpss, name: 'hpss', reason: 'explicit-method' }
case 'sample': return { fn: sample, name: 'sample', reason: 'explicit-method' }
case 'hybrid': return { fn: hybrid, name: 'hybrid', reason: 'explicit-method' }
case 'delay': return { fn: delay, name: 'delay', reason: 'explicit-method' }
case 'lpc': return { fn: lpc, name: 'lpc', reason: 'explicit-method' }
}
switch (opts?.content) {
case 'voice':
case 'speech': return { fn: psola, name: 'psola', reason: `content:${opts.content}` }
case 'tonal': return { fn: sms, name: 'sms', reason: 'content:tonal' }
default: return { fn: transient, name: 'transient', reason: 'fallback:transient' }
}
}
function notifyDecision(opts, params, decision) {
if (typeof opts?.onDecision !== 'function') return
opts.onDecision({
method: decision.name,
reason: decision.reason,
ratio: params.ratio,
semitones: params.semitones,
content: opts?.content,
formant: !!opts?.formant,
})
}
// `ratio` is a function/Float32Array whenever pitch varies over time — never identity,
// regardless of its value at t=0 (matches the atoms' makePitchShift.isIdentity).
function isVariableRatio(opts) {
let raw = opts?.ratio
return typeof raw === 'function' || raw instanceof Float32Array
}
function isIdentity(opts) {
if (isVariableRatio(opts)) return false
return resolveRatio(opts).ratio === 1
}
// `formant: true` wraps whichever method runs (README: "Wrap in formant preservation") —
// but shift-formant is a single self-contained algorithm (its own peak-lock shift fused
// with envelope extraction/reimposition), not a post-processor any other method's output
// can be piped through. True wrap semantics would need a new cross-package pipeline (STFT-
// analyze an arbitrary algorithm's output, re-impose the original's envelope on it); absent
// that, silently discarding an explicit `method` is the wrong failure mode — fail loudly.
function decide(opts) {
let { ratio } = resolveRatio(opts)
let params = { ratio, semitones: opts?.semitones ?? 0 }
if (opts?.formant) {
let method = opts?.method
if (method != null && method !== 'formant') {
let name = typeof method === 'function' ? (method.name || 'custom') : method
throw new TypeError(`pitchShift: \`formant: true\` conflicts with explicit \`method: '${name}'\` — choose one`)
}
let decision = { fn: formant, name: 'formant', reason: 'formant:true' }
notifyDecision(opts, params, decision)
return decision
}
let decision = selectMethod(opts)
notifyDecision(opts, params, decision)
return decision
}
function shiftAuto(data, opts) {
return decide(opts).fn(data, opts)
}
function createWriter(opts) {
let writer = decide(opts).fn(opts)
if (typeof writer !== 'function') {
throw new TypeError('pitchShift: selected streaming method must return a writer')
}
return writer
}
export default function pitchShift(data, opts) {
if (data instanceof Float32Array || isChannelArray(data)) {
return mapInput(data, shiftAuto, opts)
}
opts = normalizeOptionsInput(data)
if (isIdentity(opts)) return createChannelWriter(() => passThroughWriter())
return createChannelWriter(() => createWriter(opts))
}