'From Squeak3.8 of ''5 May 2005'' [latest update: #6665] on 1 May 2006 at 10:59:25 am'! "Change Set: WikiPhone Date: 30 April 2006 Author: Takashi Yamamiya HTTP based VoIP module based on Comanche. (KomServices, KomHttpServer, DynamicBindings are needed) WPServer startOn: 9090. WPServer allShutDown. url _ 'http://localhost:9090/phone'. url _ 'http://languagegame.org:9090/phone'. url _ 'http://208.49.99.244/wikiphone/phone'. WPPhone connectUrl: url. WPPhone disconnectUrl: url. WPPhone sendUrl: url sound: PluckedSound bachFugue. (WPTextChatClient url: 'http://languagegame.org:9090/') openAsMorph openInHand " Preferences enable: #canRecordWhilePlaying. SoundPlayer stopReverb. HttpAdaptor dataTimeout: 100. ! Object subclass: #WPChunkedMessage instanceVariableNames: 'chunks' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! HttpResponse subclass: #WPChunkedResponse instanceVariableNames: 'message destroyBlock' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Server'! Object subclass: #WPClient instanceVariableNames: 'url id isSending isReceiving nextIndex senderQueue' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! Object subclass: #WPMessage instanceVariableNames: 'status headers contents' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! Object subclass: #WPPhone instanceVariableNames: 'url client isRecording players recorder recordCodec recordIndex stat' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! !WPPhone commentStamp: 'tak 4/29/2006 16:30' prior: 0! WPPhone connectUrl: 'http://localhost:9090/phone'. WPPhone disconnectUrl: 'http://localhost:9090/phone'. self sendUrl: 'http://localhost:9090/phone' sound: FMSound majorChord Structure: url String -- url of the connection client WPClient -- client object isRecording Boolean -- whether recording loop is running player WPQueueSound -- sound queue recorder WPSoundRecorder -- stat Array -- statistic information session String -- Identifier of the client. ! QueueSound subclass: #WPQueueSound instanceVariableNames: 'delayBuffer delayBufferSize isWaiting codec isWaitDone' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! !WPQueueSound commentStamp: 'tak 4/25/2006 14:18' prior: 0! To avoid to cut off the sound, this sound queue keep a couple of sound in a buffer.! Object subclass: #WPRouter instanceVariableNames: 'receivers mutex' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Server'! !WPRouter commentStamp: 'tak 4/30/2006 22:16' prior: 0! Structure: receivers Set -- a set of shared queue ! HttpService subclass: #WPServer instanceVariableNames: 'routers' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Server'! !WPServer commentStamp: 'tak 5/1/2006 00:01' prior: 0! WPServer2 startOn: 9091. WPServer2 allShutDown. ! SoundInputStream subclass: #WPSoundRecorder instanceVariableNames: 'handler' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! !WPSoundRecorder commentStamp: 'tak 4/25/2006 20:06' prior: 0! It hooks input sound buffers of SoundInputStream. Is there any sophiscated way?! TestCase subclass: #WPTest instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Server'! Object subclass: #WPTextChatClient instanceVariableNames: 'outStream client localName' classVariableNames: '' poolDictionaries: '' category: 'WikiPhone-Client'! !WPTextChatClient commentStamp: '' prior: 0! (self url: 'http://localhost:9090/') openAsMorph openInHand Structure: outStream TranscriptStream -- out put text. client WPClient -- network client. localName String -- your local host name. ! !HttpRequest methodsFor: 'initialize-release' stamp: 'tak 4/23/2006 01:30'! readRequestHeaderFrom: aStream | reqHeader | reqHeader := ReadStream on: aStream upToEmptyLine. self initStatusString: reqHeader upToLineEnd. self header: (self class parseHttpHeader: reqHeader upToEnd). self rawPostFields! ! !PositionableStream methodsFor: 'accessing' stamp: 'tak 4/23/2006 01:22'! upToEmptyLine "Line delimiter might be LF or CRLF. It is useful for parse HTTP header." | writer line | writer := WriteStream on: (String new: 100). [line := self upTo: Character lf. line isEmpty or: [line = String cr]] whileFalse: [writer nextPutAll: line. writer nextPut: Character lf]. ^ writer contents! ! !PositionableStream methodsFor: 'accessing' stamp: 'tak 4/23/2006 01:27'! upToLineEnd "Line delimiter might be LF or CRLF. It is useful for parse HTTP header." | line | line := self upTo: Character lf. (line isEmpty not and: [line last = Character cr]) ifTrue: [^ line allButLast]. ^ line! ! !SocketStream methodsFor: 'accessing' stamp: 'tak 4/24/2006 11:02'! printOn: aStream | title | title := self class name. aStream nextPutAll: (title first isVowel ifTrue: ['an '] ifFalse: ['a ']); nextPutAll: title. aStream space. collection ifNotNil: [self contents printOn: aStream]! ! !SoundCodec methodsFor: '*OpenAL' stamp: 'pbm 3/15/2006 14:52'! decodeCompressedDataNoReset: aByteArray "Decode the entirety of the given encoded data buffer with this codec. Answer a monophonic SoundBuffer containing the uncompressed samples." | frameCount result increments | frameCount := self frameCount: aByteArray. result := SoundBuffer newMonoSampleCount: frameCount * self samplesPerFrame. "self reset." increments := self decodeFrames: frameCount from: aByteArray at: 1 into: result at: 1. ((increments first = aByteArray size) and: [increments last = result size]) ifFalse: [ self error: 'implementation problem; increment sizes should match buffer sizes']. ^ result ! ! !SoundCodec methodsFor: '*OpenAL' stamp: 'pbm 3/15/2006 15:37'! encodeSoundBufferNoReset: aSoundBuffer "Encode the entirety of the given monophonic SoundBuffer with this codec. Answer a ByteArray containing the compressed sound data." | codeFrameSize frameSize fullFrameCount lastFrameSamples result increments finalFrame i lastIncs | frameSize := self samplesPerFrame. fullFrameCount := aSoundBuffer monoSampleCount // frameSize. lastFrameSamples := aSoundBuffer monoSampleCount - (fullFrameCount * frameSize). codeFrameSize := self bytesPerEncodedFrame. codeFrameSize = 0 ifTrue: ["Allow room for 1 byte per sample for variable-length compression" codeFrameSize := frameSize]. lastFrameSamples > 0 ifTrue: [result := ByteArray new: (fullFrameCount + 1) * codeFrameSize] ifFalse: [result := ByteArray new: fullFrameCount * codeFrameSize]. " self reset." increments := self encodeFrames: fullFrameCount from: aSoundBuffer at: 1 into: result at: 1. lastFrameSamples > 0 ifTrue: [ finalFrame := SoundBuffer newMonoSampleCount: frameSize. i := fullFrameCount * frameSize. 1 to: lastFrameSamples do: [:j | finalFrame at: j put: (aSoundBuffer at: (i := i + 1))]. lastIncs := self encodeFrames: 1 from: finalFrame at: 1 into: result at: 1 + increments second. increments := Array with: increments first + lastIncs first with: increments second + lastIncs second]. increments second < result size ifTrue: [^ result copyFrom: 1 to: increments second] ifFalse: [^ result] ! ! !WPChunkedMessage methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 21:14'! initialize super initialize. chunks _ SharedQueue new.! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:34'! addChunk: aStringOrByteArray self addMessage: (WPMessage contents: aStringOrByteArray)! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:34'! addMessage: message chunks nextPut: message! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 4/30/2006 23:04'! chunks ^ chunks! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 4/30/2006 23:52'! chunks: aSharedQueue ^ chunks := aSharedQueue! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:34'! endChunk chunks nextPut: nil! ! !WPChunkedMessage methodsFor: 'accessing' stamp: 'tak 5/1/2006 01:27'! writeOn: aStream | message | [message := chunks next. message isEmptyOrNil] whileFalse: [message writeChunkOn: aStream]. aStream nextPutAll: '0' , String crlf. aStream nextPutAll: String crlf. aStream flush! ! !WPChunkedMessage class methodsFor: 'accessing' stamp: 'tak 4/30/2006 21:56'! nextChunkOn: aStream "Answer next chunk data, or nil if it is the last chunk" | header length contents | header := aStream upToLineEnd. length := ('16r' , header asUppercase) asNumber. length = 0 ifTrue: [aStream next: 4. ^ nil]. contents := aStream next: length. aStream next: 2. ^ contents! ! !WPChunkedMessage class methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:50'! nextMessageHeaderFrom: aString | header reader key value | header := Dictionary new. reader := aString readStream. "skip length" reader upTo: $;. [reader atEnd] whileFalse: [key := reader upTo: $=. value := reader upTo: $;. header at: key put: value]. ^ header! ! !WPChunkedMessage class methodsFor: 'accessing' stamp: 'tak 5/1/2006 01:55'! nextMessageOn: aStream "Answer next chunk data, or nil if it is the last chunk" | headerString length message | message := WPMessage new. headerString := aStream upToLineEnd. headerString isEmpty ifTrue: [^ nil]. length := ('16r' , headerString asUppercase) asNumber. length = 0 ifTrue: [aStream next: 2. ^ nil]. message headers: (self nextMessageHeaderFrom: headerString). message contents: (aStream next: length). aStream next: 2. ^ message! ! !WPChunkedResponse methodsFor: 'accessing' stamp: 'tak 4/30/2006 23:39'! message ^ message! ! !WPChunkedResponse methodsFor: 'accessing' stamp: 'tak 5/1/2006 02:02'! whenDestroyDo: aBlock destroyBlock := aBlock! ! !WPChunkedResponse methodsFor: 'responding' stamp: 'tak 4/30/2006 21:39'! pvtWriteContentLengthOn: aStream aStream nextPutAll: 'Transfer-Encoding: chunked', String crlf.! ! !WPChunkedResponse methodsFor: 'responding' stamp: 'tak 4/30/2006 23:39'! pvtWriteContentsOn: aStream message writeOn: aStream! ! !WPChunkedResponse methodsFor: 'initialize-release' stamp: 'tak 5/1/2006 02:03'! destroy destroyBlock ifNotNil: [destroyBlock value]. ^ super destroy! ! !WPChunkedResponse methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 23:47'! initialize message := WPChunkedMessage new. contents := '' readStream. contentType := MIMEDocument contentTypeHtml! ! !WPChunkedResponse class methodsFor: 'instance creation' stamp: 'tak 4/30/2006 23:42'! new ^ super basicNew initialize! ! !WPClient methodsFor: 'accessing' stamp: 'tak 5/1/2006 00:18'! path ^ url fullPath! ! !WPClient methodsFor: 'accessing' stamp: 'tak 5/1/2006 00:19'! url: aString url := aString asUrl! ! !WPClient methodsFor: 'connecting' stamp: 'tak 5/1/2006 00:17'! openConnection ^ SocketStream openConnectionToHost: (NetNameResolver addressForName: url authority timeout: 20) port: (url port ifNil: [80])! ! !WPClient methodsFor: 'connecting' stamp: 'tak 5/1/2006 02:22'! receiverLoop: aBlock socket: socket | message headerReader | socket nextPutAll: 'GET ' , self path , ' HTTP/1.1' , String crlf. socket nextPutAll: 'Host: ' , url authority , String crlf. socket nextPutAll: String crlf. headerReader := ReadStream on: socket upToEmptyLine. headerReader. [message := WPChunkedMessage nextMessageOn: socket. message isEmptyOrNil or: [isReceiving not]] whileFalse: [aBlock value: message]. "needs keep alive test..." ^ true! ! !WPClient methodsFor: 'connecting' stamp: 'tak 5/1/2006 03:54'! senderLoop: socket "Send one message, anser true if the server wants to keep alive" | response request | socket nextPutAll: 'PUT ' , self path , ' HTTP/1.1' , String crlf. socket nextPutAll: 'Host: ' , url authority , String crlf. socket nextPutAll: 'Transfer-Encoding: chunked', String crlf. socket nextPutAll: String crlf. request := WPChunkedMessage new. request chunks: senderQueue. request writeOn: socket. response := WPMessage initializeFromStream: socket. ^ response isKeep! ! !WPClient methodsFor: 'connecting' stamp: 'tak 4/30/2006 23:53'! send: message message from: id. senderQueue nextPut: message! ! !WPClient methodsFor: 'connecting' stamp: 'tak 5/1/2006 00:19'! startReceiverLoop: aBlock | socket | [isReceiving] whileTrue: [socket := self openConnection. socket buffered: false. [[[isReceiving and: [self receiverLoop: aBlock socket: socket]] whileTrue] on: ConnectionTimedOut do: [:ex | ex]] ensure: [socket close]]! ! !WPClient methodsFor: 'connecting' stamp: 'tak 4/30/2006 23:06'! startReceiver: aBlock | process | isReceiving := true. process := [self startReceiverLoop: aBlock] forkAt: Processor userBackgroundPriority. process name: self name, '(receiver)'.! ! !WPClient methodsFor: 'connecting' stamp: 'tak 4/30/2006 23:06'! startSender | process | isSending := true. process := [self startSenderLoop] forkAt: Processor userBackgroundPriority. process name: self name, '(sender)'.! ! !WPClient methodsFor: 'connecting' stamp: 'tak 5/1/2006 00:23'! startSenderLoop | socket | [isSending] whileTrue: [socket := self openConnection. [[isSending and: [self senderLoop: socket]] whileTrue] ensure: [socket close]]! ! !WPClient methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 23:06'! close self stopSending. self stopReceiving. ! ! !WPClient methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 23:06'! initialize id := UUID new asString. isSending := false. isReceiving := false. nextIndex := nil. senderQueue := SharedQueue new! ! !WPClient methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 23:06'! stopReceiving isReceiving := false. ! ! !WPClient methodsFor: 'initialize-release' stamp: 'tak 5/1/2006 01:14'! stopSending isSending := false. self send: WPMessage end. ! ! !WPClient class methodsFor: 'examples' stamp: 'tak 5/1/2006 02:25'! ping: url count: count size: size wait: waitSec timeout: timeoutSec "Answer {avarage round trip time. number of successed}" "WPClient ping: 'http://localhost:9090' count: 5 size: 1000 wait: 10 timeout: 500" | client semaphore start successed results end current total elapse | semaphore := Semaphore new. successed := 0. "result is an array of {start time. end time}" results := Array new: count. client := WPClient url: url. client startReceiver: [:message | end := Time millisecondClockValue. successed := successed + 1. current := results at: successed. current at: 2 put: end. successed >= count ifTrue: [semaphore signal]]. 1 to: count do: [:i | (Delay forMilliseconds: waitSec) wait. start := Time millisecondClockValue. results at: i put: {start. nil}. client send: (WPMessage contents: (String new: size))]. [semaphore waitTimeoutMSecs: timeoutSec] ensure: [client close]. total := results inject: 0 into: [:subtotal :next | next second ifNotNil: [elapse := next second - next first. subtotal + elapse]]. ^ {total / count. successed}! ! !WPClient class methodsFor: 'examples' stamp: 'tak 5/1/2006 02:25'! sendTimeStamp "self sendTimeStamp" | client | client := WPClient url: 'http://localhost:9090'. client send: (WPMessage contents: TimeStamp now printString , String cr)! ! !WPClient class methodsFor: 'examples' stamp: 'tak 5/1/2006 02:24'! showPing: url "health check for the url, and show result into Transcript" "WPClient showPing: 'http://localhost:9090'" | size result successed avarage speed start | size := 1000. start _ Time millisecondClockValue. result := WPClient ping: url count: 10 size: size wait: 10 timeout: 5000. avarage := result first. successed := result second. speed := size * successed / (Time millisecondClockValue - start) * 1000. Transcript cr; show: 'success: ' , successed. Transcript show: ' avarage: ' , (avarage roundTo: 0.1) printString , ' ms'. Transcript show: ' speed: ' , (speed roundTo: 0.1) printString , ' byte/s'. ^ avarage! ! !WPClient class methodsFor: 'examples' stamp: 'tak 5/1/2006 02:25'! startTimeStamp "self startTimeStamp" | client | client := WPClient url: 'http://localhost:9090'. [[client send: (WPMessage contents: TimeStamp now printString , String cr). (Delay forSeconds: 1) wait] repeat] forkAt: Processor userBackgroundPriority! ! !WPClient class methodsFor: 'instance creation' stamp: 'tak 4/30/2006 23:06'! url: aString ^ self new url: aString! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 14:58'! contentLength ^ headers at: 'content-length' ifAbsent: [0]! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 14:56'! contents ^ contents! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 14:58'! contents: aString ^ contents _ aString! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/29/2006 17:29'! from ^ headers at: 'from' ifAbsent: ['']! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/29/2006 17:28'! from: aString ^ headers at: 'from' put: aString! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/27/2006 16:56'! headers ^ headers! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 02:32'! headers: aDictionary ^ headers _ aDictionary! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 5/1/2006 00:39'! isEmptyOrNil ^ contents isEmptyOrNil! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 02:24'! isKeep "self new isKeep: (Dictionary new add: ('keep-alive' -> 'timeout=10, max=1'); yourself)" | aString reader | aString := headers at: 'keep-alive' ifAbsent: [^ false]. reader := aString readStream. reader upToAll: 'max='. ^ reader upToEnd asNumber > 1! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 02:23'! isOK ^ status size = 15 and: [(status at: 10) = $2]! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/25/2006 04:15'! nextIndex ^ headers at: 'next-index' ifAbsent: []! ! !WPMessage methodsFor: 'accessing' stamp: 'tak 4/24/2006 02:22'! status: aString ^ status _ aString! ! !WPMessage methodsFor: 'initialize-release' stamp: 'tak 4/27/2006 16:42'! initialize headers _ Dictionary new.! ! !WPMessage methodsFor: 'read/write' stamp: 'tak 4/30/2006 22:44'! writeChunkOn: aStream | length | length := contents size. length printOn: aStream base: 16. headers keysAndValuesDo: [:key :value | aStream nextPut: $;. aStream nextPutAll: key. aStream nextPut: $=. aStream nextPutAll: value]. aStream nextPutAll: String crlf. aStream nextPutAll: contents. aStream nextPutAll: String crlf. aStream flush! ! !WPMessage methodsFor: 'read/write' stamp: 'tak 4/28/2006 03:01'! writeOn: aSocketStream aSocketStream nextPutAll: 'Content-Length: ' , contents size printString , String crlf. headers keysAndValuesDo: [:key :value | aSocketStream nextPutAll: key , ': ' , value , String crlf]. aSocketStream nextPutAll: String crlf. aSocketStream nextPutAll: contents! ! !WPMessage class methodsFor: 'instance creation' stamp: 'tak 4/27/2006 16:39'! contents: aString | message | message _ self new. message contents: aString. ^ message! ! !WPMessage class methodsFor: 'instance creation' stamp: 'tak 5/1/2006 01:10'! end ^ self contents: ''! ! !WPMessage class methodsFor: 'instance creation' stamp: 'tak 4/27/2006 16:31'! initializeFromStream: aStream | response headerReader | response := self new. headerReader := ReadStream on: aStream upToEmptyLine. response status: headerReader upToLineEnd. response headers: (HttpRequest parseHttpHeader: headerReader upToEnd). response contents: (aStream next: response contentLength). ^ response! ! !WPPhone methodsFor: 'accessing' stamp: 'tak 4/29/2006 16:52'! client ^ client! ! !WPPhone methodsFor: 'accessing' stamp: 'tak 4/24/2006 13:14'! getStatistics: data "stat = {time. size. count}" | now elapse size count description lastTime | self. stat ifNil: [stat := {Time millisecondClockValue. 0. 0}]. lastTime := stat first. now := Time millisecondClockValue. elapse := now - lastTime. size := stat second + data size. count := stat third + 1. stat := {lastTime. size. count}. elapse > 0 ifFalse: [^ self]. description := (size / elapse * 1000.0) rounded asString , ' byte/s ' , (count / elapse * 1000 roundTo: 0.1) asString , ' req/s'. description displayAt: 0 @ 0. elapse > 10000 ifTrue: [stat := nil]! ! !WPPhone methodsFor: 'accessing' stamp: 'tak 4/25/2006 21:20'! url ^ url! ! !WPPhone methodsFor: 'accessing' stamp: 'tak 5/1/2006 02:25'! url: aString url := aString. client := WPClient url: aString! ! !WPPhone methodsFor: 'sound' stamp: 'tak 4/30/2006 00:28'! flushFor: fromID | player | " Transcript show: ' ::flush'." player := self playerFor: fromID. players removeKey: fromID asSymbol. player flush. player done: true! ! !WPPhone methodsFor: 'sound' stamp: 'tak 4/29/2006 18:48'! playerFor: fromID | player | ^ players at: fromID asSymbol ifAbsentPut: [player := WPQueueSound new. player codec: GSMCodec new. player delayBufferSize: self class delayBufferSize. player play. player]! ! !WPPhone methodsFor: 'sound' stamp: 'tak 4/30/2006 00:28'! play: message | data player | data := message contents. self getStatistics: data. data isEmpty ifTrue: [^ self flushFor: message from]. " Transcript show: ' ->' , (message headers at: 'record-index' ifAbsent: ['']). " player := self playerFor: message from. player addBuffer: data asByteArray samplingRate: self class samplingRate! ! !WPPhone methodsFor: 'sound' stamp: 'tak 4/30/2006 00:29'! sendBuffer: buf | message aByteArray | (buf anySatisfy: [:e | e > self class thresholdLevel]) ifTrue: [recordIndex := recordIndex + 1. isRecording := true. aByteArray := recordCodec encodeSoundBufferNoReset: buf. message := WPMessage contents: aByteArray. message headers at: 'record-index' put: recordIndex asString. " Transcript cr; show: recordIndex asString , '-> '." client send: message] ifFalse: [isRecording ifTrue: [self sendNoSound. isRecording := false]]! ! !WPPhone methodsFor: 'sound' stamp: 'tak 4/28/2006 12:26'! sendNoSound client send: (WPMessage contents: '')! ! !WPPhone methodsFor: 'control' stamp: 'tak 4/28/2006 03:14'! startListening client startReceiver: [:message | self play: message]! ! !WPPhone methodsFor: 'control' stamp: 'tak 4/29/2006 18:45'! startRecording recorder ifNotNil: [recorder stopRecording]. recorder := WPSoundRecorder new. recorder samplingRate: self class samplingRate. recorder bufferSize: self class bufferSize. recorder recordLevel: 1. client startSender. recorder startRecording: [:buffer | self sendBuffer: buffer]! ! !WPPhone methodsFor: 'control' stamp: 'tak 4/28/2006 13:02'! stopListening client stopReceiving! ! !WPPhone methodsFor: 'control' stamp: 'tak 4/28/2006 13:03'! stopRecording recorder ifNotNil: [recorder stopRecording]. client stopSending ! ! !WPPhone methodsFor: 'initialize-release' stamp: 'tak 4/29/2006 18:39'! initialize isRecording := false. recordIndex := 0. players := IdentityDictionary new. recordCodec _ GSMCodec new.! ! !WPPhone methodsFor: 'initialize-release' stamp: 'tak 4/29/2006 17:05'! release [self stopRecording] on: Error do: [:ex | ex]. self stopListening. players ifNotNil: [players do: [:each | each done: true]]. super release! ! !WPPhone class methodsFor: 'constants' stamp: 'tak 4/29/2006 22:45'! bufferSize ^ 1600 * 5! ! !WPPhone class methodsFor: 'constants' stamp: 'tak 4/30/2006 00:27'! delayBufferSize ^ 1! ! !WPPhone class methodsFor: 'constants' stamp: 'tak 4/28/2006 12:28'! samplingRate ^ SoundPlayer samplingRate! ! !WPPhone class methodsFor: 'constants' stamp: 'tak 4/25/2006 15:10'! thresholdLevel ^ 200! ! !WPPhone class methodsFor: 'examples' stamp: 'tak 4/25/2006 15:05'! delayTestUrl: aString sound: sound "Start phone client listening only, and play sound locally and remote" "self delayTestUrl: 'http://localhost:9090/phone' sound: FMSound majorChord" "self delayTestUrl: 'http://localhost:9090/phone' sound: PluckedSound bachFugue" | phone | [sound asSampledSound play] fork. phone := (WPPhone new url: aString) startListening. self sendUrl: aString sound: sound. Sensor waitButton. phone release! ! !WPPhone class methodsFor: 'examples' stamp: 'tak 4/29/2006 18:40'! sendUrl: aString sound: sound "Send any squeak sound to WikiPhone server" "self sendUrl: 'http://localhost:9090/phone' sound: FMSound majorChord" "self sendUrl: 'http://localhost:9090/phone' sound: PluckedSound bachFugue" | sampled buf reader split phone | sampled := sound asSampledSound. buf := sampled samples. reader := buf readStream. split := Array streamContents: [:writer | [reader atEnd] whileFalse: [writer nextPut: (reader next: WPPhone bufferSize)]]. phone := WPPhone new url: aString. phone client startSender. split do: [:each | phone sendBuffer: each. (Delay forSeconds: WPPhone bufferSize / WPPhone samplingRate * 0.9) wait]. phone sendNoSound. phone release.! ! !WPPhone class methodsFor: 'instance creation' stamp: 'tak 4/25/2006 21:38'! connectUrl: aString "Connect the url and start calling (to stop, #release is sended)." "(WPPhone connectUrl: 'http://localhost:9090/phone') inspect" | phone | self disconnectUrl: aString. phone := self new url: aString. phone startListening. phone startRecording. ^ phone! ! !WPPhone class methodsFor: 'instance creation' stamp: 'tak 4/25/2006 21:22'! disconnectUrl: aString "Connect the url and start calling (to stop, #release is sended)." "(WPPhone disconnectUrl: 'http://localhost:9090/phone') inspect" (self allInstances select: [:each | each url = aString]) do: [:each | each release]! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/29/2006 18:38'! addBuffer: aByteArray samplingRate: samplingRate | soundBuffer | soundBuffer := codec decodeCompressedDataNoReset: aByteArray. self add: (SampledSound samples: soundBuffer samplingRate: samplingRate)! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/29/2006 18:33'! add: aSound delayBuffer nextPut: aSound. delayBuffer size > delayBufferSize ifTrue: [self flush. super add: delayBuffer next]! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/29/2006 18:36'! codec: aSoundCodec codec _ aSoundCodec! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/29/2006 22:33'! delayBuffer ^ delayBuffer! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/29/2006 18:34'! delayBufferSize: aNumber delayBufferSize := aNumber! ! !WPQueueSound methodsFor: 'accessing' stamp: 'tak 4/28/2006 03:26'! flush isWaiting := false! ! !WPQueueSound methodsFor: 'initialization' stamp: 'tak 4/29/2006 21:10'! initialize super initialize. delayBuffer := SharedQueue new. delayBufferSize := 0. isWaiting := true. isWaitDone := false! ! !WPQueueSound methodsFor: 'sound generation' stamp: 'tak 4/29/2006 22:10'! nextSound "No sound fragments are unavailable, wait a moment" (sounds isEmpty and: [delayBuffer isEmpty]) ifTrue: [isWaiting := true. ^ nil]. "There are enough buffer" (isWaiting not and: [sounds isEmpty and: [delayBuffer isEmpty not]]) ifTrue: [super add: delayBuffer next]. ^ super nextSound! ! !WPQueueSound methodsFor: 'sound generation' stamp: 'tak 4/29/2006 22:44'! samplesRemaining (done and: [sounds isEmpty and: [delayBuffer isEmpty and: [currentSound isNil]]]) ifTrue: [^ 0] ifFalse: [^ 1000000]! ! !WPQueueSound class methodsFor: 'examples' stamp: 'tak 4/25/2006 18:24'! example1 "[self example1] fork" | buf reader splitted q soundStream gap soundBufSize isWait nextSound | gap := 0.1. isWait := false. soundBufSize := 4096. buf := FMSound majorChord asSampledSound samples. buf := PluckedSound bachFugue asSampledSound samples. reader := buf readStream. splitted := Array streamContents: [:writer | [reader atEnd] whileFalse: [writer nextPut: (reader next: soundBufSize)]]. q := self new bufferSize: 5. q play. "q inspect." soundStream := splitted readStream. [soundStream atEnd] whileFalse: [nextSound := soundStream next. [q add: (SampledSound samples: nextSound samplingRate: SoundPlayer samplingRate)] fork. Processor yield. isWait ifTrue: [(Delay forSeconds: soundBufSize / SoundPlayer samplingRate + gap) wait]]. q done: true! ! !WPRouter methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:18'! addQueue: aSharedQueue mutex critical: [receivers add: aSharedQueue]! ! !WPRouter methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:19'! nextPut: message mutex critical: [receivers do: [:each | each nextPut: message]]! ! !WPRouter methodsFor: 'accessing' stamp: 'tak 4/30/2006 22:19'! removeQueue: aSharedQueue mutex critical: [receivers remove: aSharedQueue]! ! !WPRouter methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 22:18'! initialize receivers _ Set new. mutex _ Semaphore forMutualExclusion. ! ! !WPRouter class methodsFor: 'constants' stamp: 'tak 4/30/2006 22:13'! keepAliveInterval ^ 60! ! !WPServer methodsFor: 'configuration' stamp: 'tak 4/30/2006 22:25'! serverDescriptionOn: strm strm nextPutAll: self serverType. ! ! !WPServer methodsFor: 'initialize-release' stamp: 'tak 4/30/2006 22:25'! initialize super initialize. routers _ Dictionary new.! ! !WPServer methodsFor: 'serving' stamp: 'tak 5/1/2006 02:10'! processGet: request | response router | router := self routersAt: request url. response := WPChunkedResponse new. response status: #ok. router addQueue: response message chunks. response whenDestroyDo: [router removeQueue: response message chunks]. ^ response! ! !WPServer methodsFor: 'serving' stamp: 'tak 4/30/2006 22:26'! processHttpRequest: request request method = 'PUT' ifTrue: [^ self processPut: request]. request method = 'GET' ifTrue: [^ self processGet: request]! ! !WPServer methodsFor: 'serving' stamp: 'tak 5/1/2006 02:34'! processPut: request | message router | router := self routersAt: request url. [message := WPChunkedMessage nextMessageOn: request stream. message isEmptyOrNil] whileFalse: [router nextPut: message]. ^ HttpResponse status: #created contents: ''! ! !WPServer methodsFor: 'serving' stamp: 'tak 5/1/2006 02:26'! routersAt: pathName ^ routers at: pathName ifAbsentPut: [WPRouter new]! ! !WPServer class methodsFor: 'class initialization' stamp: 'tak 5/1/2006 02:26'! allShutDown "WPServer allShutDown" WPServer services do: [:each | each unregister]. (Process allInstances select: [:each | '*WPServer*' match: each name]) do: [:each | each terminate]! ! !WPSoundRecorder methodsFor: 'accessing' stamp: 'tak 4/27/2006 16:50'! emitBuffer: aSampledSound handler ifNotNil: [handler value: aSampledSound]! ! !WPSoundRecorder methodsFor: 'accessing' stamp: 'tak 4/27/2006 16:51'! startRecording: aBlock handler := aBlock. ^ super startRecording! ! !WPTest methodsFor: 'testing' stamp: 'tak 4/30/2006 21:54'! testChunkedMessage "self debug: #testChunkedMessage" | m response reader | m := WPChunkedMessage new. m addChunk: 'This is the first chunked message.'. m addChunk: 'This is the second chunked a bit loooooooooooooonger message.'. m endChunk. response := String streamContents: [:s | m writeOn: s]. self assert: response size = 112. reader := response readStream. self assert: (WPChunkedMessage nextChunkOn: reader) = 'This is the first chunked message.'. self assert: (WPChunkedMessage nextChunkOn: reader) = 'This is the second chunked a bit loooooooooooooonger message.'. self assert: (WPChunkedMessage nextChunkOn: reader) = nil! ! !WPTest methodsFor: 'testing' stamp: 'tak 4/30/2006 22:44'! testChunkedMessage2 "self debug: #testChunkedMessage2" | m response reader m1 m2 r1 r2 | m := WPChunkedMessage new. m1 _ WPMessage contents: 'This is the first chunked message.'. m1 headers at: 'From' put: 'Alice'. m2 _ WPMessage contents: 'This is the second chunked a bit loooooooooooooonger message.'. m2 headers at: 'From' put: 'Bob'. m addMessage: m1. m addMessage: m2. m endChunk. response := String streamContents: [:s | m writeOn: s]. self assert: response size = 132. reader := response readStream. r1 _ WPChunkedMessage nextMessageOn: reader. self assert: r1 contents = 'This is the first chunked message.'. self assert: (r1 headers at: 'From') = 'Alice'. r2 _ WPChunkedMessage nextMessageOn: reader. self assert: r2 contents = 'This is the second chunked a bit loooooooooooooonger message.'. self assert: (r2 headers at: 'From') = 'Bob'. self assert: (WPChunkedMessage nextChunkOn: reader) = nil! ! !WPTest methodsFor: 'testing' stamp: 'tak 4/29/2006 22:43'! testQueue "self debug: #testQueue" | q | q _ WPQueueSound new. q delayBufferSize: 1. q add: (FMSound default duration: 0.1). self assert: q sounds size = 0. self assert: q delayBuffer size = 1. q add: (FMSound default duration: 0.2). self assert: q sounds size = 1. self assert: q delayBuffer size = 1. q add: (FMSound default duration: 0.3). self assert: q sounds size = 2. self assert: q delayBuffer size = 1. q done: true. self assert: q samplesRemaining = 1000000. self assert: q nextSound duration = 0.1. self assert: q samplesRemaining = 1000000. self assert: q nextSound duration = 0.2. self assert: q samplesRemaining = 1000000. self assert: q nextSound duration = 0.3. self assert: q samplesRemaining = 0. ! ! !WPTest methodsFor: 'testing' stamp: 'tak 4/23/2006 01:22'! testUpToEmptyLine "self debug: #testUpToEmptyLine" | input | input := 'test' , String crlf , String crlf. self assert: input readStream upToEmptyLine = ('test' , String crlf). input := 'test' , String lf , String lf. self assert: input readStream upToEmptyLine = ('test' , String lf). input := 'test' , String crlf , 'test' , String crlf , String crlf. self assert: input readStream upToEmptyLine = ('test' , String crlf , 'test' , String crlf). input := 'test' , String lf , 'test' , String lf , String lf. self assert: input readStream upToEmptyLine = ('test' , String lf , 'test' , String lf)! ! !WPTest methodsFor: 'testing' stamp: 'tak 4/23/2006 01:27'! testUpToLineEnd "self debug: #testUpToLineEnd" | input | input := 'test' , String crlf. self assert: input readStream upToLineEnd = 'test'. input := 'test' , String lf. self assert: input readStream upToLineEnd = 'test'! ! !WPTest class methodsFor: 'Testing' stamp: 'tak 4/30/2006 23:40'! processChunkedRequest: request | response | response := WPChunkedResponse new. response status: #ok. response message addChunk: 'abcdefghijhlmnlpqrstuvwxyz'. response message addChunk: 'ABCDEFGHIJKLMNOPQRSTUVWXYXZ'. response message endChunk. ^ response! ! !WPTest class methodsFor: 'Testing' stamp: 'tak 4/30/2006 22:10'! runChunkedTestServer "self runChunkedTestServer" "HttpService allInstances do: [:each | each unregister]. (Process allInstances select: [:each | '*Chunked Test*' match: each name]) do: [:each | each terminate]" (HttpService on: 8080 named: 'Chunked Test') onRequestDo: [:httpRequest | WPTest processChunkedRequest: httpRequest]; start! ! !WPTextChatClient methodsFor: 'accessing' stamp: 'tak 4/27/2006 16:41'! contents: aString | contents | contents := self localName , ': ' , aString , String cr. client send: (WPMessage contents: (contents convertToWithConverter: UTF8TextConverter new) asByteArray). ^ true! ! !WPTextChatClient methodsFor: 'accessing' stamp: 'tak 4/24/2006 11:13'! localName "self new localName" ^ localName ifNil: [localName := NetNameResolver nameForAddress: NetNameResolver localHostAddress timeout: 5]! ! !WPTextChatClient methodsFor: 'accessing' stamp: 'tak 4/24/2006 17:31'! show: aString outStream show: (aString convertFromWithConverter: UTF8TextConverter new)! ! !WPTextChatClient methodsFor: 'initialize-release' stamp: 'tak 5/1/2006 02:25'! connectUrl: aString client := WPClient url: aString. client startReceiver: [:message | self show: message contents]. client startSender.! ! !WPTextChatClient methodsFor: 'initialize-release' stamp: 'tak 4/24/2006 02:06'! initialize outStream _ TranscriptStream on: (String new: 200). ! ! !WPTextChatClient methodsFor: 'initialize-release' stamp: 'tak 4/24/2006 00:31'! openAsMorph "self new openAsMorph openInHand" | window transcript input | window := (SystemWindow labelled: self className) model: self. input := PluggableTextMorph on: self text: nil accept: #contents: readSelection: nil menu: #codePaneMenu:shifted:. input acceptOnCR: true. input hideScrollBarsIndefinitely. transcript := PluggableTextMorph on: outStream text: nil accept: nil readSelection: nil menu: #codePaneMenu:shifted:. window addMorph: transcript frame: (0 @ 0 corner: 1 @ 0.8). window addMorph: input frame: (0 @ 0.8 corner: 1 @ 1). ^ window! ! !WPTextChatClient methodsFor: 'initialize-release' stamp: 'tak 4/27/2006 14:52'! release client close! ! !WPTextChatClient class methodsFor: 'instance creation' stamp: 'tak 4/24/2006 00:30'! url: aString ^ self new connectUrl: aString! !