// Text of project Serenade written on 4/29/97 at 6:31 PM // Beginning of file Serenade form // Before Script for "base" // kNoteFrame converts from note symbols to frequencies; it is handy for creating songs, so that you don't need // to type in these numbers all the time :-) "cs2" would mean C sharp, in the second octave. DefConst('kNoteFrame, {a1:440.000, as1:466.162, b1:493.882, c1:523.252, cs1:554.365, d1:587.330, ds1:622.252, e1:659.256, f1:698.456, fs1:739.988, g1:783.992, gs1:830.610, a2:880.000, as2:932.325, b2:987.765, c2:1046.50, cs2:1108.73, d2:1174.66, ds2:1244.50, e2:1318.51, f2:1396.91, fs2:1479.98, g2:1567.98, gs2:1661.22}); base := {viewBounds: {left: 1, top: 43, right: 187, bottom: 190}, SingSong: func(song, pitch, tempo, callback) begin // This function plays the given song, modified by pitch and tempo, and will call the // given callback when the song is done. // song is a frame with two slots: // notes (an array of containing any mixture of symbols, integers or reals, to set the frequency.) // durations (an array of integers or reals). // pitch and tempo are factors used to raise or lower the pitch and tempo. For example, setting // pitch to 2 will raise the notes by an octave. Set it to 1 to not change anything. local ch, note, thisnote, index, tone; // open our sound channel :CloseChannel(); // just in case there was one open, close it. ch:={_proto:protoSoundChannel}; // default is to have this be a channel for output ch:open(); ch:SetVolume(GetUserConfig('soundVolume)); // use the global sound volume channel:=ch; // save the channel in our base view, so that we can be sure it gets closed when our app closes (if not sooner) tone := toneType.viewValue; // which type of tone are they wanting to use? This gets passed to :MakeNote(). for index := 0 to length(song.notes)-1 do begin note:=song.notes[index]; if isSymbol(note) then // if the caller gave a symbol for the note, then note:=kNoteFrame.(note); // translate it to the frequency // generate the note, modifying the pitch and tempo as requested thisNote := :MakeNote(rinttol(note * pitch), rinttol(song.durations[index] * tempo), tone); // queue it up to be played try channel:Schedule(thisNote); onexception |evt.ex| do begin GetRoot():Notify(kNotifyAlert, kAppName, "Couldn't schedule note - are you using a Newton device which has no synthesizer?"); return; end; end; // now make a zero-length sound which will generate a call to the callback, so the channel can be closed. channel:Schedule({sndFrameType: 'codec, codecName: "TDTMFCodec", samples: :MakeNote(1,0,0), callback: callback}); // start the song playing asynchronously! channel:start(true); end, channel: nil // holds the currently playing sound channel, or nil if none, set in SingSong and CloseChannel , viewQuitScript: // must return the value of inherited:?ViewQuitScript() func() begin // close our sound channel if it's open. This is important - you'll leak system memory otherwise. :CloseChannel(); inherited:?ViewQuitScript(); end, CloseChannel: func() // close our sound channel if it's open. This is important - you'll leak system memory otherwise. begin local ch; if channel then begin ch:=channel; // Copy the value so we can immediately set channel to nil. This way we don't risk channel:=nil; // having any other queued sounds try to close down this same channel in their callbacks. // shut down the channel. ch:stop(); ch:close(); end; end, MakeNote: // There's an art to creating good samples. The ones below are quite simplistic but will hopefully // illustrate some of the possibilities. They are defined in this slot because the function must // change them - and how they are changed depends on which synth type they are. // The cryptic comments above the hex are a quick reference to the order of the synthesizer parameters, lined up // with their values in the string right below. Check the documentation for explanations of what they mean. // (The "\" is above the last character in a field; for example, "amp" (i.e. Amplitude) is set to 0x4710.) // This defines a simple sine wave tone DefConst('kSimpleTone, MakeBinaryFromHex( //header \freq \amp\lds\atk\dcy\sus\rel\pek\trl "0001000000000000000103E8000047100000000A000A012C000A5D000000", 'TDTMFCodec)); // This combines two sine waves; the code below adds a lower frequency to the note, making it richer. // The effect sounds better for mid-range frequencies; for low tones it's barely noticeable and // for high tones it separates into the two notes and doesn't sound right. DefConst('kChordTone, MakeBinaryFromHex( // \freq \amp\lds\atk\dcy\sus\rel\pek\trl\2freq \amp\lds\atk\dcy\sus\rel\pek\trailing silence "0001000000000000000203E8000047100000000A000A012C000A5D00000003E8000008000000000a000A0000000A04000000", 'TDTMFCodec)); // This defines a modulated sine wave - sounds less "plastic" than the simple tone, particularly on longer notes DefConst('kModulatedTone, MakeBinaryFromHex( // \freq \amp\lds\atk\dcy\sus\rel\pek\trl\2freq \amp\lds\atk\dcy\sus\rel\pek\trailing silence "0001000100000000000203E8000047100000000A001A012C000A5D00000000040000003A00000020000400000000003A0000", 'TDTMFCodec)); DefConst('kToneArray, [kSimpleTone, kChordTone, kModulatedTone]); // don't change order without changing the code below... func(frequency, duration, tone) begin local snd; snd:=clone(kToneArray[tone]); // make a copy that we can modify // subtract the attack, decay and release time from duration duration := duration - (ExtractWord(snd, 18) + ExtractWord(snd, 20) + ExtractWord(snd,24)); if duration <1 then duration := 1; // don't go negative... // change the frequency and duration as requested, for the main tone. StuffWord(snd, 10, frequency); StuffWord(snd, 22, duration); if tone=1 then // add in the lower frequency StuffWord(snd, 30, frequency div 2); if tone=1 or tone=2 then // set the duration for the second tone StuffWord(snd, 42, duration); snd; end, debug: "base", _proto: @180 }; tempo := {labelCommands: ["Very slow", "Slow", "Medium", "Fast", "Very fast"], text: "Tempo", viewBounds: {left: 9, top: 33, right: 157, bottom: 47}, pickActionScript: func(itemSelected) begin viewValue := itemSelected; inherited:?pickActionScript(itemSelected); // this method is defined internally end, textSetup: func() // default item is chosen at build-time labelCommands[viewValue], viewValue: 2, indent: 60, debug: "tempo", _proto: @190 }; AddStepForm(base, tempo); StepDeclare(base, tempo, 'tempo); pitch := {labelCommands: ["Soprano", "Alto", "Tenor", "Bass"], text: "Pitch", viewBounds: {left: 9, top: 55, right: 157, bottom: 69}, textSetup: func() // default item is chosen at build-time labelCommands[viewValue], viewValue: 2, pickActionScript: func(itemSelected) begin viewValue := itemSelected; inherited:?pickActionScript(itemSelected); // this method is defined internally end, indent: 60, debug: "pitch", _proto: @190 }; AddStepForm(base, pitch); StepDeclare(base, pitch, 'pitch); toneType := {labelCommands: ["Simple sine wave", "Deeper sine", "Modulated"], text: "Tone", viewBounds: {left: 9, top: 77, right: 157, bottom: 91}, pickActionScript: func(itemSelected) begin viewValue := itemSelected; inherited:?pickActionScript(itemSelected); // this method is defined internally end, textSetup: func() // default item is chosen at build-time labelCommands[viewValue], viewValue: 1, indent: 60, debug: "toneType", _proto: @190 }; AddStepForm(base, toneType); StepDeclare(base, toneType, 'toneType); whichsong := { labelCommands: // This array defines the songs, with their description, notes & durations [ { item: "Happy Birthday", notes: '[a1,a1,b1,a1,d1,cs1, a1,a1,b1,a1,e1,d1, a1,a1,a2,fs1,d1,cs1,b1, g1,g1,fs1,d1,e1,d1], durations: [300,100,400,400,400,800, 300,100,400,400,400,800, 300,100,400,400,400,400,1200, 300,100,400,400,400,800 ], }, { item: "Doe, a deer...", notes: '[c1,d1,e1,c1,e1,c1,e1, d1,e1,f1,f1,e1,d1,f1, e1,f1,g1,e1,g1,e1,g1, f1,g1,a2,a2,g1,f1,a2, g1,c1,d1,e1,f1,g1,a2, a2,d1,e1,f1,g1,a2,b2, b2,e1,f1,g1,a2,b2,c2, b2,b2,a2,f1,b2,g1,c2,g1,e1,d1,c1], durations: [600,200,600,200,400,400,800, 600,200,200,200,200,200,1600, 600,200,600,200,400,400,800, 600,200,200,200,200,200,1600, 600,200,200,200,200,200,1600, 600,200,200,200,200,200,1600, 600,200,200,200,200,200,1200, 200,200,400,400,400,400,400,400,400,400,800], }], text: "Song", viewBounds: {left: 9, top: 99, right: 182, bottom: 113} , indent: 60, pickActionScript: func(itemSelected) begin viewValue := itemSelected; inherited:?pickActionScript(itemSelected); // this method is defined internally end, textSetup: func() // default item is chosen at build-time labelCommands[viewValue].item, viewValue: 1, debug: "whichsong", _proto: @190 }; AddStepForm(base, whichsong); StepDeclare(base, whichsong, 'whichsong); play := { buttonClickScript: func() begin // This button is a two-state button. Normally the text is "Sing!" (the contents of the SingText slot). // If it is, then the song is started, and the text is changed to "Stop singing!". // If it's not "Sing!" then the song is stopped and the state is reset, by manually calling the callback. local myself:= self; // this is who we want our callback to send a message to // This callback is executed when the song is done playing. It closes down the channel and resets this button's state. local callback := func(state, result) begin myself:CloseChannel(); SetValue(myself, 'text, myself.SingText); end; if StrEqual(self.text, StopText) then begin // don't really need StrEqual, but it's safer this way. call callback with (nil, nil); // stop the music, and close down the channel... it doesn't use the parameters anyway. end else begin :SingSong(whichsong.labelcommands[whichsong.viewValue], 1+((3-pitch.viewValue) * 0.50), 1+((2-tempo.viewValue) * 0.4), callback); setvalue(self, 'text, StopText); end; end, text: "", viewBounds: {left: 55, top: 126, right: 137, bottom: 141}, StopText: "Stop singing!", SingText: "Sing!", debug: "play", _proto: @226 }; AddStepForm(base, play); // After Script for "play" thisView := play; // Perhaps this is odd UI, but it's better than two buttons of which only one is visible at a time... thisview.text := thisview.SingText; _view000 := {title: kAppName, viewBounds: {left: 0, top: 0, right: 78, bottom: 17}, _proto: @229 }; AddStepForm(base, _view000); // After Script for "base" thisView := base; // Copyright 1997 Apple Computer, Inc. All rights reserved. constant |layout_Serenade form| := base; // End of file Serenade form