1+ import { previewInstrumentById } from "./PreviewInstrumentPacks.js" ;
2+
13const CHORD_TONES = {
24 A : [ "A" , "C#" , "E" ] ,
35 Am : [ "A" , "C" , "E" ] ,
@@ -59,7 +61,7 @@ export class PreviewSynthEngine {
5961 return Boolean ( audioContextCtor ( this . window ) ) ;
6062 }
6163
62- async playGridRange ( { endStep, grid, label, loop = false , mode = "section" , startStep, tempoBpm = 120 } = { } ) {
64+ async playGridRange ( { endStep, grid, label, laneSettings = { } , loop = false , mode = "section" , startStep, tempoBpm = 120 } = { } ) {
6365 this . stop ( ) ;
6466 if ( ! grid ?. ok ) {
6567 return { message : "Preview Synth needs a normalized instrument grid before playback." , ok : false , reason : "missing-grid" } ;
@@ -68,11 +70,12 @@ export class PreviewSynthEngine {
6870 if ( ! contextResult . ok ) {
6971 return contextResult ;
7072 }
71- const playableEvents = this . playableEventsForRange ( grid , startStep , endStep ) ;
72- if ( ! playableEvents . length ) {
73+ const playable = this . playableEventsForRange ( grid , startStep , endStep , laneSettings ) ;
74+ if ( ! playable . events . length ) {
7375 return {
7476 message : `No playable Preview Synth notes found for ${ mode } ${ label || "(unnamed)" } . Generate or enter chords, bass, pad, lead, or drum cells before playing.` ,
7577 ok : false ,
78+ warnings : playable . warnings ,
7679 reason : "no-playable-notes"
7780 } ;
7881 }
@@ -81,19 +84,21 @@ export class PreviewSynthEngine {
8184 const secondsPerStep = secondsPerBeat / grid . subdivision ;
8285 const cycleSeconds = Math . max ( ( endStep - startStep + 1 ) * secondsPerStep , secondsPerStep ) ;
8386 this . playing = true ;
84- this . scheduleEvents ( { context : contextResult . context , events : playableEvents , secondsPerBeat, secondsPerStep, startStep } ) ;
87+ this . scheduleEvents ( { context : contextResult . context , events : playable . events , secondsPerBeat, secondsPerStep, startStep } ) ;
8588 if ( loop ) {
8689 this . loopTimer = this . window . setInterval ( ( ) => {
87- this . scheduleEvents ( { context : contextResult . context , events : playableEvents , secondsPerBeat, secondsPerStep, startStep } ) ;
90+ this . scheduleEvents ( { context : contextResult . context , events : playable . events , secondsPerBeat, secondsPerStep, startStep } ) ;
8891 } , cycleSeconds * 1000 ) ;
8992 }
9093 return {
91- eventCount : playableEvents . length ,
94+ activeLanes : playable . activeLanes ,
95+ eventCount : playable . events . length ,
9296 label,
9397 mode,
9498 ok : true ,
9599 soundFontPlayback : false ,
96- synthName : "Preview Synth"
100+ synthName : "Preview Synth" ,
101+ warnings : playable . warnings
97102 } ;
98103 }
99104
@@ -128,22 +133,51 @@ export class PreviewSynthEngine {
128133 }
129134 }
130135
131- playableEventsForRange ( grid , startStep = 0 , endStep = 0 ) {
132- return ( grid . timeline || [ ] ) . filter ( ( event ) => {
136+ playableEventsForRange ( grid , startStep = 0 , endStep = 0 , laneSettings = { } ) {
137+ const instruments = laneSettings . instruments || { } ;
138+ const muted = laneSettings . muted || { } ;
139+ const soloed = laneSettings . soloed || { } ;
140+ const soloedLanes = Object . entries ( soloed ) . filter ( ( entry ) => entry [ 1 ] ) . map ( ( [ lane ] ) => lane ) ;
141+ const warnings = [ ] ;
142+ const warningKeys = new Set ( ) ;
143+ const events = [ ] ;
144+ ( grid . timeline || [ ] ) . forEach ( ( event ) => {
133145 const stepIndex = Number ( event . stepIndex ) ;
134- return Number . isFinite ( stepIndex )
135- && stepIndex >= startStep
136- && stepIndex <= endStep
137- && this . frequenciesForEvent ( event ) . length > 0 ;
146+ if ( ! Number . isFinite ( stepIndex ) || stepIndex < startStep || stepIndex > endStep ) {
147+ return ;
148+ }
149+ if ( muted [ event . lane ] || ( soloedLanes . length && ! soloedLanes . includes ( event . lane ) ) ) {
150+ return ;
151+ }
152+ const instrumentId = String ( instruments [ event . lane ] || "" ) . trim ( ) ;
153+ const instrument = previewInstrumentById ( instrumentId ) ;
154+ if ( ! instrument ) {
155+ const key = `missing:${ event . lane } ` ;
156+ if ( ! warningKeys . has ( key ) ) {
157+ warnings . push ( `Missing preview instrument selection for ${ event . lane } . Choose a Preview Synth instrument before playback.` ) ;
158+ warningKeys . add ( key ) ;
159+ }
160+ return ;
161+ }
162+ if ( this . frequenciesForEvent ( event , instrument ) . length > 0 ) {
163+ events . push ( { ...event , previewInstrument : instrument } ) ;
164+ }
138165 } ) ;
166+ return {
167+ activeLanes : Array . from ( new Set ( events . map ( ( event ) => event . lane ) ) ) ,
168+ events,
169+ warnings
170+ } ;
139171 }
140172
141173 scheduleEvents ( { context, events, secondsPerBeat, secondsPerStep, startStep } ) {
142174 const now = context . currentTime ;
143175 events . forEach ( ( event ) => {
144176 const offsetSeconds = Math . max ( 0 , ( event . stepIndex - startStep ) * secondsPerStep ) ;
145- const durationSeconds = Math . max ( 0.06 , Number ( event . durationBeats || 1 ) * secondsPerBeat * 0.82 ) ;
146- this . frequenciesForEvent ( event ) . forEach ( ( frequency , index ) => {
177+ const instrument = event . previewInstrument ;
178+ const durationScale = Number ( instrument ?. durationScale || 1 ) ;
179+ const durationSeconds = Math . max ( 0.06 , Number ( event . durationBeats || 1 ) * secondsPerBeat * 0.82 * durationScale ) ;
180+ this . frequenciesForEvent ( event , instrument ) . forEach ( ( frequency , index ) => {
147181 this . scheduleTone ( {
148182 context,
149183 durationSeconds,
@@ -158,8 +192,8 @@ export class PreviewSynthEngine {
158192 scheduleTone ( { context, durationSeconds, event, frequency, startTime } ) {
159193 const oscillator = context . createOscillator ( ) ;
160194 const gainNode = context . createGain ( ) ;
161- const waveform = this . waveformForEvent ( event ) ;
162- const volume = this . volumeForEvent ( event ) ;
195+ const waveform = this . waveformForEvent ( event , event . previewInstrument ) ;
196+ const volume = this . volumeForEvent ( event , event . previewInstrument ) ;
163197 const endTime = startTime + durationSeconds ;
164198 oscillator . type = waveform ;
165199 oscillator . frequency . setValueAtTime ( frequency , startTime ) ;
@@ -174,33 +208,31 @@ export class PreviewSynthEngine {
174208 this . nodes . push ( { gainNode, oscillator } ) ;
175209 }
176210
177- frequenciesForEvent ( event ) {
211+ frequenciesForEvent ( event , instrument = null ) {
212+ const transposeFactor = 2 ** ( Number ( instrument ?. transposeSemitones || 0 ) / 12 ) ;
178213 if ( event . kind === "drum" ) {
179214 const frequency = DRUM_FREQUENCIES [ String ( event . value || "" ) . toLowerCase ( ) ] ;
180- return frequency ? [ frequency ] : [ ] ;
215+ return frequency ? [ frequency * transposeFactor ] : [ ] ;
181216 }
182217 if ( event . kind === "chord" ) {
183- return chordNotes ( event . value ) . map ( ( note ) => noteFrequency ( note ) ) . filter ( Boolean ) ;
218+ return chordNotes ( event . value ) . map ( ( note ) => noteFrequency ( note ) ) . filter ( Boolean ) . map ( ( frequency ) => frequency * transposeFactor ) ;
184219 }
185220 if ( event . kind === "note" ) {
186221 const frequency = noteFrequency ( event . value ) ;
187- return frequency ? [ frequency ] : [ ] ;
222+ return frequency ? [ frequency * transposeFactor ] : [ ] ;
188223 }
189224 return [ ] ;
190225 }
191226
192- waveformForEvent ( event ) {
227+ waveformForEvent ( event , instrument = null ) {
193228 if ( event . kind === "drum" ) {
194- return event . value === "kick" || event . value === "tom" ? "sine" : "square" ;
229+ return event . value === "kick" || event . value === "tom" ? "sine" : instrument ?. waveform || "square" ;
195230 }
196- return event . lane === "lead" ? "sawtooth" : event . lane === "bass" ? "triangle" : "sine" ;
231+ return instrument ?. waveform || "sine" ;
197232 }
198233
199- volumeForEvent ( event ) {
200- if ( event . kind === "drum" ) {
201- return 0.12 ;
202- }
203- return event . kind === "chord" ? 0.045 : 0.075 ;
234+ volumeForEvent ( event , instrument = null ) {
235+ return Number ( instrument ?. volume || ( event . kind === "chord" ? 0.045 : 0.075 ) ) ;
204236 }
205237
206238 stop ( ) {
0 commit comments