'From Squeak3.1alpha of 28 February 2001 [latest update: #4109] on 31 May 2001 at 12:07:23 am'! "Change Set: MPEGPlayer-jm Date: 30 May 2001 Author: John Maloney This is a reworked MPEG and MP3 player with improved performance. To try it, do: MPEGMoviePlayerMorph new openInWorld Then use the 'open' button to open an MPEG movie file."! Morph subclass: #MPEGDisplayMorph instanceVariableNames: 'frameBuffer mpegFile running desiredFrameRate allowFrameDropping startMSecs startFrame soundTrack volume endMSecs ' classVariableNames: '' poolDictionaries: '' category: 'MPEG-Player'! !MPEGDisplayMorph commentStamp: '' prior: 0! I am a simple display screen for an MPEG movie player. My step method advances the movie according to the current frame rate. If necessary, frames as skipped to maintain the desired frame rate. However, since even skipping frames takes time, it may not be possible to achieve fast frame rates with large frame sizes on slow machines. ! AlignmentMorph subclass: #MPEGMoviePlayerMorph instanceVariableNames: 'moviePlayer positionSlider ' classVariableNames: '' poolDictionaries: '' category: 'MPEG-Player'! !MPEGMoviePlayerMorph commentStamp: '' prior: 0! I provide the user-interface for playing MPEG movies, including play/stop/rewind buttons and volume and position sliders. To create an instance of me, evaluate: MPEGMoviePlayerMorph new openInWorld Then use the "open" button to open an MPEG movie file. This class supplies the front panel; the real work is done by MPEGDisplayMorph and StreamingMP3Sound. ! AbstractSound subclass: #StreamingMP3Sound instanceVariableNames: 'mpegHandle mpegStreamIndex mpegSamplingRate volume totalSamples mixer ' classVariableNames: '' poolDictionaries: '' category: 'MPEG-Player'! !StreamingMP3Sound commentStamp: '' prior: 0! I implement a streaming player for MPEG or MP3 files. Example of use: (StreamingMP3Sound openFile: 'song.mp3') play. ! StreamingMP3Sound class instanceVariableNames: ''! !MPEGDisplayMorph methodsFor: 'initialization' stamp: 'jm 5/30/2001 23:05'! initialize super initialize. super extent: 320@240. frameBuffer _ mpegFile _ nil. desiredFrameRate _ 10.0. allowFrameDropping _ true. running _ false. volume _ 0.5. ! ! !MPEGDisplayMorph methodsFor: 'file open/close' stamp: 'jm 2/21/2001 21:51'! closeFile "Close my MPEG file, if any." mpegFile ifNotNil: [ mpegFile closeFile. mpegFile _ nil. frameBuffer _ nil]. self changed. ! ! !MPEGDisplayMorph methodsFor: 'file open/close' stamp: 'jm 3/22/2001 12:42'! mpegFileIsOpen "Answer true if I have an open, valid MPEG file handle. If the handle is not valid, try to re-open the file." mpegFile ifNil: [^ false]. mpegFile fileHandle ifNil: [ "try to reopen the file, which may have been saved in a snapshot" mpegFile openFile: mpegFile fileName. mpegFile fileHandle ifNil: [mpegFile _ nil]]. ^ mpegFile notNil ! ! !MPEGDisplayMorph methodsFor: 'file open/close' stamp: 'jm 4/24/2001 12:51'! openFileNamed: mpegFileName "Try to open the MPEG file with the given name. Answer true if successful." | e secs | self closeFile. (FileDirectory default fileExists: mpegFileName) ifFalse: [self inform: 'File not found: ', mpegFileName. ^ false]. (MPEGFile isFileValidMPEG: mpegFileName) ifFalse: [self inform: 'Not an MPEG file: ', mpegFileName. ^ false]. mpegFile _ MPEGFile openFile: mpegFileName. mpegFile fileHandle ifNil: [^ false]. mpegFile hasVideo ifTrue: [ desiredFrameRate _ mpegFile videoFrameRate: 0. mpegFile hasAudio ifTrue: [ "compute frame rate from audio track" secs _ (mpegFile audioSamples: 0) asFloat / (mpegFile audioSampleRate: 0). desiredFrameRate _ (mpegFile videoFrames: 0) / secs]. e _ (mpegFile videoFrameWidth: 0)@(mpegFile videoFrameHeight: 0). frameBuffer _ Form extent: e depth: (Display depth max: 16). super extent: e. self nextFrame]. ! ! !MPEGDisplayMorph methodsFor: 'file open/close' stamp: 'jm 4/6/2001 02:49'! openMPEGFile "Invoked by the 'Open' button. Prompt for a file name and try to open that file as an MPEG file." | result fullName | result _ StandardFileMenu oldFile. result ifNil: [^ self]. fullName _ result directory pathName, FileDirectory slash, result name. self openFileNamed: fullName. ! ! !MPEGDisplayMorph methodsFor: 'drawing' stamp: 'jm 3/20/2001 15:57'! areasRemainingToFill: aRectangle "Drawing optimization. Since I completely fill my bounds with opaque pixels, this method tells Morphic that it isn't necessary to draw any morphs covered by me." ^ aRectangle areasOutside: self bounds ! ! !MPEGDisplayMorph methodsFor: 'drawing' stamp: 'jm 4/2/2001 21:54'! drawOn: aCanvas "Draw the current frame image, if there is one. Otherwise, fill screen with gray." frameBuffer ifNil: [aCanvas fillRectangle: self bounds color: Color gray] ifNotNil: [aCanvas drawImage: frameBuffer at: bounds origin]. ! ! !MPEGDisplayMorph methodsFor: 'drawing' stamp: 'jm 2/21/2001 21:18'! extent: aPoint "Do nothing; my extent is determined by my frameBuffer form." ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 4/6/2001 08:41'! nextFrame "Fetch the next frame into the frame buffer." mpegFile ifNil: [^ self]. frameBuffer depth = 16 ifTrue: [ mpegFile videoReadNext16BitFrameInto: frameBuffer bits width: frameBuffer width height: frameBuffer height stream: 0] ifFalse: [ mpegFile videoReadNextFrameInto: frameBuffer bits width: frameBuffer width height: frameBuffer height stream: 0]. self changed. ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 4/6/2001 08:31'! previousFrame "Go to the previous frame." | n | mpegFile ifNil: [^ self]. running ifTrue: [^ self]. n _ (mpegFile videoGetFrame: 0) - 2. n _ (n min: ((mpegFile videoFrames: 0) - 3)) max: 0. mpegFile videoSetFrame: n stream: 0. self nextFrame. ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 4/6/2001 08:45'! rewindMovie "Rewind to the beginning of the movie." "Details: Seeking by percent or frame number both seem to have problems, so just re-open the file." | savedRate | self mpegFileIsOpen ifFalse: [^ self]. self stopPlaying. savedRate _ desiredFrameRate. "hold onto rate, in case user set it" self openFileNamed: mpegFile fileName. "recomputes rate" desiredFrameRate _ savedRate. "restore user's rate" ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 3/21/2001 16:25'! setFrameRate "Ask the user to specify the desired frame rate." | rateString | rateString _ FillInTheBlank request: 'Desired frames per second?' initialAnswer: desiredFrameRate printString. rateString size = 0 ifTrue: [^ self]. desiredFrameRate _ rateString asNumber asFloat. desiredFrameRate <= 0.1 ifTrue: [desiredFrameRate _ 0.1]. ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 5/30/2001 23:53'! startPlaying "Start playing the movie at the current position." running _ false. self mpegFileIsOpen ifFalse: [^ self]. soundTrack ifNotNil: [soundTrack pause]. SoundPlayer stopReverb. soundTrack _ StreamingMP3Sound new initMPEGHandle: mpegFile streamIndex: 0. soundTrack volume: volume. soundTrack resumePlaying. running _ true. startFrame _ mpegFile videoGetFrame: 0. startMSecs _ Time millisecondClockValue. ! ! !MPEGDisplayMorph methodsFor: 'commands' stamp: 'jm 4/2/2001 21:55'! stopPlaying "Stop playing the movie." running _ false. soundTrack ifNotNil: [soundTrack pause]. endMSecs _ Time millisecondClockValue. ! ! !MPEGDisplayMorph methodsFor: 'stepping' stamp: 'jm 5/30/2001 23:10'! step "If I'm running and the mpegFile is open and has video, advance to the next frame. Stop if we we hit the end of the video." running ifFalse: [^ self]. mpegFile ifNil: [^ self]. (mpegFile hasVideo and: [mpegFile endOfVideo: 0]) ifTrue: [ running _ false. endMSecs _ Time millisecondClockValue] ifFalse: [self advanceFrame]. ! ! !MPEGDisplayMorph methodsFor: 'stepping' stamp: 'jm 4/6/2001 08:47'! stepTime "Run my step method as often as possible. Step does very little work if it is not time to advance to the next frame." ^ 0 ! ! !MPEGDisplayMorph methodsFor: 'other' stamp: 'jm 5/30/2001 23:11'! advanceFrame "Advance to the next frame if it is time to do so, skipping frames if necessary." | msecs currentFrame desiredFrame framesToAdvance | mpegFile hasVideo ifFalse: [^ self]. msecs _ Time millisecondClockValue - startMSecs. desiredFrame _ startFrame + ((msecs * desiredFrameRate) // 1000). desiredFrame _ desiredFrame min: (mpegFile videoFrames: 0). currentFrame _ mpegFile videoGetFrame: 0. framesToAdvance _ desiredFrame - currentFrame. framesToAdvance <= 0 ifTrue: [^ self]. (allowFrameDropping and: [framesToAdvance > 1]) ifTrue: [ mpegFile videoDropFrames: framesToAdvance - 1 stream: 0]. self nextFrame. ! ! !MPEGDisplayMorph methodsFor: 'other' stamp: 'jm 5/30/2001 23:09'! measureMaxFrameRate "For testing. Play through the movie as fast as possible, updating the world each time, and report the frame rate." | old | desiredFrameRate _ 1000.0. self rewindMovie; startPlaying. old _ allowFrameDropping. allowFrameDropping _ false. [running] whileTrue: [World doOneCycleNow]. allowFrameDropping _ old. ^ (mpegFile videoFrames: 0) / ((endMSecs - startMSecs) / 1000.0) ! ! !MPEGDisplayMorph methodsFor: 'other' stamp: 'jm 4/9/2001 01:44'! moviePosition "Answer a number between 0.0 and 1.0 indicating the current position within the movie." mpegFile ifNil: [^ 0.0]. mpegFile fileHandle ifNil: [^ 0.0]. mpegFile hasVideo ifTrue: [^ ((mpegFile videoGetFrame: 0) asFloat / (mpegFile videoFrames: 0)) min: 1.0]. mpegFile hasAudio ifTrue: [^ ((mpegFile audioGetSample: 0) asFloat /(mpegFile audioSamples: 0)) min: 1.0]. ^ 0.0 ! ! !MPEGDisplayMorph methodsFor: 'other' stamp: 'jm 4/6/2001 08:27'! moviePosition: fraction "Jump to the position the given fraction through the movie. The argument is a number between 0.0 and 1.0." | totalSamples sampleIndex lastFrame frameIndex | self mpegFileIsOpen ifFalse: [^ self]. sampleIndex _ nil. mpegFile hasAudio ifTrue: [ totalSamples _ mpegFile audioSamples: 0. sampleIndex _ ((totalSamples * fraction) truncated max: 0) min: totalSamples. mpegFile audioSetSample: sampleIndex stream: 0]. mpegFile hasVideo ifTrue: [ lastFrame _ mpegFile videoFrames: 0. sampleIndex ifNil: [frameIndex _ (lastFrame * fraction) truncated - 1] ifNotNil: [frameIndex _ (lastFrame * (sampleIndex / totalSamples)) truncated - 1]. frameIndex _ (frameIndex max: 0) min: (lastFrame - 3). mpegFile videoSetFrame: frameIndex stream: 0. ^ self nextFrame]. ! ! !MPEGDisplayMorph methodsFor: 'other' stamp: 'jm 5/30/2001 22:06'! volume: aNumber "Set the sound playback volume to the given level, between 0.0 and 1.0." volume _ aNumber asFloat. volume < 0.0 ifTrue: [volume _ 0.0]. volume > 1.0 ifTrue: [volume _ 1.0]. soundTrack volume: volume. ! ! !MPEGMoviePlayerMorph methodsFor: 'initialization' stamp: 'jm 5/2/2001 14:58'! initialize super initialize. self color: (Color gray: 0.9). self hResizing: #shrinkWrap; vResizing: #shrinkWrap. borderWidth _ 2. self listDirection: #topToBottom. self cornerStyle: #rounded. self layoutInset: 4. moviePlayer _ MPEGDisplayMorph new. self addMorphFront: moviePlayer. self addButtonRow. self addVolumeSlider. self addPositionSlider. self extent: 10@10. "make minimum size" ! ! !MPEGMoviePlayerMorph methodsFor: 'drawing' stamp: 'jm 5/2/2001 15:04'! drawOn: aCanvas "Optimization: Do not draw myself if the only damage is contained within the movie frame. This avoids overdraw when playing a movie." (moviePlayer bounds containsRect: aCanvas clipRect) ifFalse: [super drawOn: aCanvas]. ! ! !MPEGMoviePlayerMorph methodsFor: 'stepping' stamp: 'jm 4/6/2001 07:49'! step "Update the position slider from the current movie position." positionSlider adjustToValue: moviePlayer moviePosition. ! ! !MPEGMoviePlayerMorph methodsFor: 'stepping' stamp: 'jm 5/30/2001 23:33'! stepTime "Update the position slider a few times a second." ^ 500 ! ! !MPEGMoviePlayerMorph methodsFor: 'private' stamp: 'jm 4/6/2001 07:50'! addButtonRow | r | r _ AlignmentMorph newRow vResizing: #shrinkWrap; color: color. r addMorphBack: (self buttonName: 'Open' action: #openMPEGFile). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: 'Rewind' action: #rewindMovie). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: 'Play' action: #startPlaying). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: 'Stop' action: #stopPlaying). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: 'Rate' action: #setFrameRate). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: '<' action: #previousFrame). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). r addMorphBack: (self buttonName: '>' action: #nextFrame). r addMorphBack: (Morph new extent: 4@1; color: Color transparent). self addMorphBack: r. ! ! !MPEGMoviePlayerMorph methodsFor: 'private' stamp: 'jm 3/22/2001 15:52'! addPositionSlider | r | positionSlider _ SimpleSliderMorph new color: color; extent: 200@2; target: moviePlayer; actionSelector: #moviePosition:; adjustToValue: 0. r _ AlignmentMorph newRow color: color; layoutInset: 0; wrapCentering: #center; cellPositioning: #leftCenter; hResizing: #shrinkWrap; vResizing: #rigid; height: 24. r addMorphBack: (StringMorph contents: 'start '). r addMorphBack: positionSlider. r addMorphBack: (StringMorph contents: ' end'). self addMorphBack: r. ! ! !MPEGMoviePlayerMorph methodsFor: 'private' stamp: 'jm 3/22/2001 12:35'! addVolumeSlider | levelSlider r | levelSlider _ SimpleSliderMorph new color: color; extent: 200@2; target: moviePlayer; actionSelector: #volume:; adjustToValue: 0.5. r _ AlignmentMorph newRow color: color; layoutInset: 0; wrapCentering: #center; cellPositioning: #leftCenter; hResizing: #shrinkWrap; vResizing: #rigid; height: 24. r addMorphBack: (StringMorph contents: ' soft '). r addMorphBack: levelSlider. r addMorphBack: (StringMorph contents: ' loud'). self addMorphBack: r. ! ! !MPEGMoviePlayerMorph methodsFor: 'private' stamp: 'jm 3/20/2001 14:35'! buttonName: aString action: aSymbol ^ SimpleButtonMorph new target: moviePlayer; label: aString; actionSelector: aSymbol; color: (Color gray: 0.8). ! ! !StreamingMP3Sound methodsFor: 'initialization-release' stamp: 'jm 5/30/2001 16:51'! closeFile "Close the MP3 or MPEG file." mpegHandle ifNil: [^ self]. mpegHandle closeFile. mpegHandle _ nil. mixer _ nil. ! ! !StreamingMP3Sound methodsFor: 'initialization-release' stamp: 'jm 5/30/2001 22:18'! initFileName: fileName "Open the MP3 or MPEG file with the given name." | f | (MPEGFile isFileValidMPEG: fileName) ifFalse: [^ nil]. f _ MPEGFile openFile: fileName. self initMPEGHandle: f streamIndex: 0. "assume sound track is in stream 0" ! ! !StreamingMP3Sound methodsFor: 'initialization-release' stamp: 'jm 5/30/2001 22:18'! initMPEGHandle: anMPEGFile streamIndex: anInteger "Initialize for playing the given stream of the given MPEG or MP3 file." mpegHandle _ anMPEGFile. mpegStreamIndex _ anInteger. volume _ 0.3. totalSamples _ mpegHandle audioSamples: mpegStreamIndex. ! ! !StreamingMP3Sound methodsFor: 'accessing' stamp: 'jm 9/25/2000 20:00'! samplePosition "Answer the current sample position within the MPEG sound track." self ensureFileOpen. ^ mpegHandle audioGetSample: mpegStreamIndex ! ! !StreamingMP3Sound methodsFor: 'accessing' stamp: 'jm 5/30/2001 22:21'! samplePosition: aNumber "Seek to the current sample position within the MPEG sound track." | i | self ensureFileOpen. i _ aNumber asInteger min: totalSamples. mpegHandle audioSetSample: i stream: mpegStreamIndex. ! ! !StreamingMP3Sound methodsFor: 'accessing' stamp: 'jm 9/26/2000 08:02'! totalSamples "Answer the total number of sound samples in this sound." ^ totalSamples ! ! !StreamingMP3Sound methodsFor: 'accessing' stamp: 'jm 9/26/2000 07:49'! volume "Answer my volume." ^ volume ! ! !StreamingMP3Sound methodsFor: 'accessing' stamp: 'jm 5/30/2001 16:53'! volume: aNumber "Set my volume to the given number between 0.0 and 1.0." volume _ aNumber. self createMixer. ! ! !StreamingMP3Sound methodsFor: 'playing' stamp: 'jm 5/30/2001 22:06'! playSampleCount: n into: aSoundBuffer startingAt: startIndex "Mix the next n samples of this sound into the given buffer starting at the given index" self loadBuffersForSampleCount: (n * mpegSamplingRate) // SoundPlayer samplingRate. mixer playSampleCount: n into: aSoundBuffer startingAt: startIndex. ! ! !StreamingMP3Sound methodsFor: 'playing' stamp: 'jm 5/30/2001 16:53'! reset super reset. self createMixer. self samplePosition: 0. ! ! !StreamingMP3Sound methodsFor: 'playing' stamp: 'jm 5/30/2001 16:58'! samplesRemaining | samplesPlayed | mpegHandle ifNil: [^ 0]. samplesPlayed _ mpegHandle audioGetSample: mpegStreamIndex. samplesPlayed > totalSamples ifTrue: [^ 0]. ^ totalSamples - samplesPlayed ! ! !StreamingMP3Sound methodsFor: 'private' stamp: 'jm 5/30/2001 23:31'! createMixer "Create a mixed sound consisting of sampled sounds with one sound buffer's worth of samples. The sound has the same sampling rate and number of channels as the MPEG or MP3 file." | channels pan snd | self ensureFileOpen. channels _ mpegHandle audioChannels: mpegStreamIndex. mpegSamplingRate _ mpegHandle audioSampleRate: mpegStreamIndex. mixer _ MixedSound new. 1 to: channels do: [:c | channels = 1 ifTrue: [pan _ 0.5] ifFalse: [pan _ (c - 1) asFloat / (channels - 1)]. snd _ SampledSound samples: (SoundBuffer newMonoSampleCount: 2) "buffer size will be adjusted dynamically" samplingRate: mpegSamplingRate. mixer add: snd pan: pan volume: volume]. ! ! !StreamingMP3Sound methodsFor: 'private' stamp: 'jm 9/25/2000 20:01'! ensureFileOpen "Raise an error if there is no MPEG file handle." mpegHandle ifNil: [self error: 'No MPEG or MP3 file']. ! ! !StreamingMP3Sound methodsFor: 'private' stamp: 'jm 5/30/2001 21:34'! loadBuffersForSampleCount: count "Load the sound buffers for all tracks with the next count samples from the MPEG file sound track." | snd buf | 1 to: mixer sounds size do: [:i | snd _ mixer sounds at: i. buf _ snd samples. buf monoSampleCount = count ifFalse: [ buf _ SoundBuffer newMonoSampleCount: count. snd setSamples: buf samplingRate: mpegSamplingRate]. i = 1 ifTrue: [ "first channel" mpegHandle audioReadBuffer: buf stream: mpegStreamIndex channel: 0] ifFalse: [ "all other channels" mpegHandle audioReReadBuffer: buf stream: mpegStreamIndex channel: 0]]. mixer reset. ! ! !StreamingMP3Sound class methodsFor: 'as yet unclassified' stamp: 'jm 5/30/2001 18:05'! openFile: fileName "Answer an instance of me for playing the MPEG or MP3 file with the given name." ^ self new initFileName: fileName ! !