Skip to content

Commit 614fa93

Browse files
committed
eq curve
1 parent 23a2d4c commit 614fa93

26 files changed

+947
-186
lines changed

src/App.tsx

Lines changed: 200 additions & 130 deletions
Large diffs are not rendered by default.

src/audio/AudioEngine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ export class AudioEngine {
256256
const secondsPerBeat = 60.0 / this.bpm;
257257
const quarterNote = secondsPerBeat;
258258
switch (delayTime) {
259+
case "quarter":
260+
return quarterNote;
261+
case "half":
262+
return quarterNote * 2;
259263
case "8th":
260264
return quarterNote / 2;
261265
case "16th":

src/audio/TB303Synth.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type TB303Patch = {
1919
decay: number; // Envelope decay time in seconds
2020
accent: number; // Accent amount 0-1
2121
volume: number; // 0-1
22+
noteLength: number; // 0-1, multiplier for note duration
2223
};
2324

2425
export const defaultTB303Patch: TB303Patch = {
@@ -29,6 +30,7 @@ export const defaultTB303Patch: TB303Patch = {
2930
decay: 0.2,
3031
accent: 0.6,
3132
volume: 0.7,
33+
noteLength: 0.5,
3234
};
3335

3436
export class TB303Synth {
@@ -38,13 +40,20 @@ export class TB303Synth {
3840

3941
constructor(
4042
audioContext: AudioContext,
41-
patch: TB303Patch = defaultTB303Patch
43+
patch: TB303Patch = defaultTB303Patch,
44+
outputNode?: AudioNode
4245
) {
4346
this.audioContext = audioContext;
4447
this.patch = { ...patch };
4548
this.masterGain = audioContext.createGain();
4649
this.masterGain.gain.value = this.patch.volume;
47-
this.masterGain.connect(audioContext.destination);
50+
51+
// Connect to provided output node or default to audioContext.destination
52+
if (outputNode) {
53+
this.masterGain.connect(outputNode);
54+
} else {
55+
this.masterGain.connect(audioContext.destination);
56+
}
4857
}
4958

5059
setPatch(patch: Partial<TB303Patch>) {

src/audio/effects.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ export type EffectType =
99
| "compressor"
1010
| "limiter";
1111

12-
export type DelayTime = "8th" | "16th" | "32nd" | "triplet";
12+
export type DelayTime =
13+
| "quarter"
14+
| "half"
15+
| "8th"
16+
| "16th"
17+
| "32nd"
18+
| "triplet";
1319

1420
export type EffectSlot = {
1521
type: EffectType;
@@ -22,6 +28,9 @@ export type EffectSlot = {
2228
// Delay parameters
2329
delayTime?: DelayTime;
2430
delayFeedback?: number;
31+
delayLeft?: number;
32+
delayRight?: number;
33+
delayPingPong?: boolean;
2534
// Distortion/Saturation parameters
2635
distortionAmount?: number;
2736
distortionTone?: number;
@@ -88,6 +97,9 @@ export function getDefaultParamsForEffect(
8897
dryWet: 0.3,
8998
delayTime: "16th",
9099
delayFeedback: 0.4,
100+
delayLeft: 0.5,
101+
delayRight: 0.5,
102+
delayPingPong: false,
91103
};
92104
case "distortion":
93105
return {

src/components/FX/FX.styles.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export const EffectParams = styled.div`
8484
`;
8585

8686
export const DelayTimeButtons = styled.div`
87-
grid-column: span 2;
87+
grid-column: span 3;
8888
display: grid;
89-
grid-template-columns: repeat(4, 1fr);
89+
grid-template-columns: repeat(6, 1fr);
9090
gap: 3px;
9191
`;
9292

@@ -115,3 +115,31 @@ export const DelayTimeButton = styled.button<{ $active?: boolean }>`
115115
transform: scale(0.98);
116116
}
117117
`;
118+
119+
export const PingPongCheckbox = styled.div`
120+
grid-column: span 3;
121+
display: flex;
122+
align-items: center;
123+
gap: 6px;
124+
padding: 4px 6px;
125+
background: ${({ theme }) => theme.colors.panel};
126+
border: 1px solid ${({ theme }) => theme.colors.gridLine};
127+
border-radius: ${({ theme }) => theme.radii.sm};
128+
129+
input[type="checkbox"] {
130+
width: 14px;
131+
height: 14px;
132+
cursor: pointer;
133+
accent-color: ${({ theme }) => theme.colors.accentAlt};
134+
}
135+
136+
label {
137+
font-size: 10px;
138+
font-weight: 600;
139+
color: ${({ theme }) => theme.colors.textPrimary};
140+
cursor: pointer;
141+
user-select: none;
142+
text-transform: uppercase;
143+
letter-spacing: 0.3px;
144+
}
145+
`;

src/components/FX/FX.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,44 @@ export function FX({ effectSlots, onChange }: Props) {
7676
onChange={(v) => handleParamChange(slotIndex, { delayFeedback: v })}
7777
format={(v) => `${Math.round(v * 100)}%`}
7878
/>
79+
<Knob
80+
label="Left"
81+
min={0}
82+
max={1}
83+
step={0.01}
84+
value={effect.delayLeft ?? 0.5}
85+
onChange={(v) => handleParamChange(slotIndex, { delayLeft: v })}
86+
format={(v) => `${Math.round(v * 100)}%`}
87+
/>
88+
<Knob
89+
label="Right"
90+
min={0}
91+
max={1}
92+
step={0.01}
93+
value={effect.delayRight ?? 0.5}
94+
onChange={(v) => handleParamChange(slotIndex, { delayRight: v })}
95+
format={(v) => `${Math.round(v * 100)}%`}
96+
/>
7997
<S.DelayTimeButtons>
80-
{(['8th', '16th', '32nd', 'triplet'] as DelayTime[]).map((time) => (
98+
{(['quarter', 'half', '8th', '16th', '32nd', 'triplet'] as DelayTime[]).map((time) => (
8199
<S.DelayTimeButton
82100
key={time}
83101
$active={effect.delayTime === time}
84102
onClick={() => handleParamChange(slotIndex, { delayTime: time })}
85103
>
86-
{time === 'triplet' ? '3' : time.replace('th', '')}
104+
{time === 'triplet' ? '3' : time === 'quarter' ? '1/4' : time === 'half' ? '1/2' : time.replace('th', '')}
87105
</S.DelayTimeButton>
88106
))}
89107
</S.DelayTimeButtons>
108+
<S.PingPongCheckbox>
109+
<input
110+
type="checkbox"
111+
id={`pingpong-${slotIndex}`}
112+
checked={effect.delayPingPong ?? false}
113+
onChange={(e) => handleParamChange(slotIndex, { delayPingPong: e.target.checked })}
114+
/>
115+
<label htmlFor={`pingpong-${slotIndex}`}>Ping Pong</label>
116+
</S.PingPongCheckbox>
90117
</>
91118
)}
92119
{(effect.type === 'distortion' || effect.type === 'saturation') && (

src/components/InstrumentSelector/InstrumentSelector.styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Container = styled.div`
88
background: ${({ theme }) => theme.colors.surface};
99
border-radius: 4px;
1010
border: 1px solid ${({ theme }) => theme.colors.border};
11+
min-width: 0;
1112
`;
1213

1314
export const SectionTitle = styled.div`
@@ -41,6 +42,7 @@ export const Item = styled.button<{ $active?: boolean; $selected?: boolean }>`
4142
color: ${({ theme, $selected }) =>
4243
$selected ? theme.colors.background : theme.colors.text};
4344
font-size: 13px;
45+
min-width: 0;
4446
4547
&:hover {
4648
background: ${({ theme, $selected }) =>
@@ -61,4 +63,8 @@ export const Name = styled.div`
6163
flex: 1;
6264
text-align: left;
6365
font-weight: 500;
66+
min-width: 0;
67+
overflow: hidden;
68+
text-overflow: ellipsis;
69+
white-space: nowrap;
6470
`;

src/components/InstrumentSelector/InstrumentSelector.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ type Props = {
1010

1111
const instruments = [
1212
{ type: 'sample' as InstrumentType, name: 'Sample', icon: '🎵' },
13-
{ type: 'tb303' as InstrumentType, name: 'TB-303', icon: '🎹' },
1413
];
1514

1615
export function InstrumentSelector({ selectedPad, currentInstrument, onSelectInstrument }: Props) {

src/components/KitSelector/KitSelector.styles.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Container = styled.div`
88
border: 1px solid ${({ theme }) => theme.colors.gridLine};
99
border-radius: ${({ theme }) => theme.radii.sm};
1010
gap: 12px;
11+
min-width: 0;
1112
`;
1213

1314
export const Section = styled.div`
@@ -28,6 +29,7 @@ export const KitRow = styled.div`
2829
display: flex;
2930
align-items: center;
3031
gap: 12px;
32+
min-width: 0;
3133
`;
3234

3335
export const KitNameInput = styled.input`
@@ -50,6 +52,7 @@ export const KitNameInput = styled.input`
5052
export const KitSelector = styled.div`
5153
display: flex;
5254
gap: 2px;
55+
flex-shrink: 0;
5356
`;
5457

5558
export const KitButton = styled.button<{ $active: boolean }>`
@@ -122,6 +125,7 @@ export const PresetItem = styled.button<{ $active?: boolean }>`
122125
cursor: pointer;
123126
transition: all 0.15s ease;
124127
text-align: left;
128+
min-width: 0;
125129
126130
&:hover {
127131
background: ${({ theme, $active }) =>
@@ -141,6 +145,7 @@ export const PresetImage = styled.img`
141145
border-radius: ${({ theme }) => theme.radii.sm};
142146
background: ${({ theme }) => theme.colors.background};
143147
border: 1px solid ${({ theme }) => theme.colors.gridLine};
148+
flex-shrink: 0;
144149
`;
145150

146151
export const PresetImagePlaceholder = styled.div`
@@ -154,9 +159,14 @@ export const PresetImagePlaceholder = styled.div`
154159
justify-content: center;
155160
font-size: 10px;
156161
color: ${({ theme }) => theme.colors.textSecondary};
162+
flex-shrink: 0;
157163
`;
158164

159165
export const PresetName = styled.span`
160166
flex: 1;
161167
line-height: 1.4;
168+
min-width: 0;
169+
overflow: hidden;
170+
text-overflow: ellipsis;
171+
white-space: nowrap;
162172
`;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import styled, { keyframes } from "styled-components";
2+
3+
const spin = keyframes`
4+
0% {
5+
transform: rotate(0deg);
6+
}
7+
100% {
8+
transform: rotate(360deg);
9+
}
10+
`;
11+
12+
export const Overlay = styled.div`
13+
position: absolute;
14+
top: 0;
15+
left: 0;
16+
right: 0;
17+
bottom: 0;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
background: rgba(0, 0, 0, 0.7);
22+
backdrop-filter: blur(4px);
23+
z-index: 100;
24+
border-radius: ${({ theme }) => theme.radii.md};
25+
`;
26+
27+
export const SpinnerContainer = styled.div`
28+
display: flex;
29+
flex-direction: column;
30+
align-items: center;
31+
gap: 16px;
32+
`;
33+
34+
export const Spinner = styled.div`
35+
width: 60px;
36+
height: 60px;
37+
border: 4px solid ${({ theme }) => theme.colors.gridLine};
38+
border-top-color: ${({ theme }) => theme.colors.accent};
39+
border-radius: 50%;
40+
animation: ${spin} 0.8s linear infinite;
41+
`;
42+
43+
export const Message = styled.p`
44+
color: ${({ theme }) => theme.colors.text};
45+
font-family: ${({ theme }) => theme.typography.mono};
46+
font-size: 14px;
47+
margin: 0;
48+
`;

0 commit comments

Comments
 (0)