// License : GNU/GPL 2+ VAR_INPUT volume : INT; base_speed : TIME; song_DB : BLOCK_DB; END_VAR VAR notes_index : INT; octave_shift : INT; sharp : BOOL; tie : BOOL; wait_time_active : BOOL; wait_timestamp : TIME; prev_note_id : BYTE; prev_note_value : BYTE; END_VAR VAR_TEMP cur_note : BYTE; cur_note_id : BYTE; cur_note_value : BYTE; base_freq : DINT; deci_hz : INT; period : WORD; remaining_ms : TIME; now : TIME; END_VAR BEGIN // Get the current time CALL SFC 64 ( RET_VAL := #now ) // Check if we need to wait U #wait_time_active SPBN NWAI L #now L #wait_timestamp -D SLD 1 // sign extension SSD 1 // sign extension L 0 I SPB NEND L 0 T #notes_index L DBB 0 T #cur_note NEND: NOP 0 // Extract note ID L #cur_note UW W#16#0F T #cur_note_id // Extract note value L #cur_note UW W#16#70 SRW 4 T #cur_note_value // If this is a special note, handle it. L #cur_note_id L W#16#F ==I SPB SPEC // Check if the current note is equal to the previous note. L #cur_note_id L #prev_note_id ==I SPB SAME // Tune the note. // Get the note frequency in deci-Hz. TUNE: AUF "DB_notes" U #sharp SPBN NOSH AUF "DB_notes_sharp" NOSH: L #cur_note_id SLW 4 LAR1 L DBW [AR1, P#0.0] T #deci_hz // Octave shift the frequency. L #octave_shift L 0 >=I SPB POSO // Negative octave shift L #octave_shift NEGI L #deci_hz SRW SPA OCTE // Positive octave shift POSO: L #octave_shift L #deci_hz SLW OCTE: T #deci_hz // Calculate the PWM period from the frequency. L #deci_hz L 0 ==I SPB NPER L L#20000000 // pwm_baseFreqHz * 10 L #deci_hz /D NPER: T #period // Write period to PWM output hardware CALL "FC_setPWM" ( period := #period, volume := #volume, ) // Calculate the wait timestamp from the current note value. CALL "FC_calcWaitTime" ( now := #now, note_value := #cur_note_value, base_speed := #base_speed, wait_timestamp := #wait_timestamp, wait_time_active := #wait_time_active, ) // We are done with this note. // Reset all flags. CLR = #sharp = #tie L #cur_note_id T #prev_note_id L #cur_note_value T #prev_note_value SPA NEXT // If this is a special flags note, handle it. SPEC: L #cur_note_value SPL INVA // Invalid SPA SHRP // Activate "sharp" (Kreuz) SPA DOT // Activate "dot" (Punktierung) SPA TIE // Activate "tie" (Haltebogen) SPA SHUP // Shift one octave up SPA SHDN // Shift one octave down SPA INVA // Invalid SPA INVA // Invalid SPA INVA // Invalid INVA: BEA // Activate "sharp" (Kreuz) SHRP: SET = #sharp SPA NEXT // Activate "dot" (Punktierung) // Get the value of the previous note (that is the currenly tuned one) // and wait for half its time. DOT: L #prev_note_value + 1 // Decrease value by 50% T #prev_note_value CALL "FC_calcWaitTime" ( now := #now, note_value := #prev_note_value, base_speed := #base_speed, wait_timestamp := #wait_timestamp, wait_time_active := #wait_time_active, ) SPA NEXT // Activate "tie" (Haltebogen) TIE: SET = #tie SPA NEXT // Shift one octave up SHUP: L #octave_shift + 1 T #octave_shift SPA NEXT // Shift one octave down SHDN: L #octave_shift + -1 T #octave_shift SPA NEXT // The current note is the same as the previous one. // If "tie" is not active, deactivate the PWM and wait // a tiny amount of time. Then re-activate the note. // If "tie" is active, tune the note. SAME: U #tie SPB TUNE CALL "FC_setPWM" ( period := W#16#0, volume := #volume, ) L #now L T#30ms +D UD DW#16#7FFFFFFF T #wait_timestamp SET = #wait_time_active // Set "tie" to re-activate the note // after the wait time has passed. SET = #tie SPB NINC // Increment notes index NEXT: L #notes_index + 1 T #notes_index NINC: BE END_FUNCTION_BLOCK FUNCTION "FC_setPWM" : VOID VAR_INPUT period : WORD; volume : INT; END_VAR VAR_TEMP volume_shift : INT; END_VAR BEGIN // Calculate the duty cycle shift from the volume. // #volume can be 0-15 L 15 L #volume -I + 1 T #volume_shift // Write period to PWM output hardware L #period T "pwm_period" // Write duty cycle to PWM output hardware L #volume_shift L #period SRW T "pwm0" END_FUNCTION FUNCTION "FC_calcWaitTime" : VOID VAR_INPUT now : TIME; note_value : BYTE; base_speed : TIME; END_VAR VAR_OUTPUT wait_timestamp : TIME; wait_time_active : BOOL; END_VAR VAR_TEMP remaining_ms : TIME; END_VAR BEGIN // Calculate the remaining time from the note value. L #note_value L #base_speed // base speed, in milliseconds SRW T #remaining_ms // Calculate the next timestamp to wait for. L #now L #remaining_ms +D UD DW#16#7FFFFFFF T #wait_timestamp SET = #wait_time_active END_FUNCTION ]]>